def test_intersect_simple(self, app_data: TimelineData, afk_data: TimelineData, inclusive_results: TimelineResults, exclusive_results: TimelineResults) -> None: app_events = [ Event(2, get_date(data[0]), data[1], { 'app': 'Browser', 'title': 'website - Browser' }) for data in app_data ] afk_events = [ Event(2, get_date(data[0]), data[1], {'status': 'not-afk'}) for data in afk_data ] create_function = Timeline.create_from_bucket_events app_timeline = create_function(BucketType.APP, app_events) afk_timeline = create_function(BucketType.AFK, afk_events) app_timeline.intersect(afk_timeline, EventsAnalyzer.app_afk_timeline_condition) self.assert_timeline(app_timeline, inclusive_results) app_timeline = create_function(BucketType.APP, app_events) afk_timeline = create_function(BucketType.AFK, afk_events) app_timeline.intersect(afk_timeline, EventsAnalyzer.app_afk_timeline_condition, False) self.assert_timeline(app_timeline, exclusive_results)
def get_events(bucket_id: str) -> List[Event]: if bucket_id == 'window': return [ Event(1, get_date(1), 1, {'app': 'Another', 'title': 'whatever'}), Event(2, get_date(3), 2, {'app': 'Another2', 'title': 'whatever'}), Event(3, get_date(6), 5, { 'app': 'Browser', 'title': 'website - Browser', }), Event(4, get_date(12), 6, { 'app': 'Browser', 'title': 'whatever - Browser', }), ] elif bucket_id == 'afk': return [ Event(1, get_date(1), 3, {'status': 'afk'}), Event(2, get_date(5), 8, {'status': 'not-afk'}), ] elif bucket_id == 'browser': return [ Event(1, get_date(1), 3, {'title': 'nothing1'}), Event(2, get_date(5), 4, {'title': 'nothing2'}), Event(3, get_date(10), 5, {'title': 'website'}), Event(4, get_date(16), 2, {'title': 'nothing3'}), ] else: return []
def test_split_event_on_hour() -> None: e = Event(timestamp=datetime(2019, 1, 1, 11, 30, tzinfo=timezone.utc), duration=timedelta(minutes=1)) assert len(split_event_on_hour(e)) == 1 e = Event(timestamp=datetime(2019, 1, 1, 11, 30, tzinfo=timezone.utc), duration=timedelta(hours=2)) split_events = split_event_on_hour(e) assert len(split_events) == 3
def _redact_event(e: Event, pattern: Union[str, Pattern]) -> Event: e = deepcopy(e) for k, v in e.data.items(): if isinstance(v, str): if isinstance(pattern, str): if pattern in v.lower(): e.data[k] = REDACTED else: if pattern.findall(v.lower()): e.data[k] = REDACTED return e
def test_get_cached_events(self) -> None: mock_events1 = [ Event(4, get_date(16), 2, { 'app': 'x', 'title': 'nothing3' }), Event(3, get_date(10), 5, { 'app': 'x', 'title': 'website' }), Event(2, get_date(5), 4, { 'app': 'x', 'title': 'nothing2' }), Event(1, get_date(1), 3, { 'app': 'x', 'title': 'nothing1' }), ] mock_events2 = [ Event(6, get_date(27), 2, { 'app': 'x', 'title': 'nothing3' }), Event(5, get_date(21), 5, { 'app': 'x', 'title': 'website' }), Event(4, get_date(16), 4, { 'app': 'x', 'title': 'nothing3' }), Event(3, get_date(10), 5, { 'app': 'x', 'title': 'website' }), ] self.client_mock.get_events = MagicMock( side_effect=[mock_events1, mock_events2]) time = datetime.now() events = self.repository.get_events('window', time, time) self.check_events([ ('x', 'nothing3', 16, 2), ('x', 'website', 10, 5), ('x', 'nothing2', 5, 4), ('x', 'nothing1', 1, 3), ], events) events = self.repository.get_events('window', time, time) self.check_events([ ('x', 'nothing3', 27, 2), ('x', 'website', 21, 5), ('x', 'nothing3', 16, 4), ('x', 'website', 10, 5), ('x', 'nothing2', 5, 4), ('x', 'nothing1', 1, 3), ], events)
def create_fake_events(start: datetime, end: datetime) -> Iterable[Event]: assert start.tzinfo assert end.tzinfo # First set RNG seeds to make the notebook reproducible random.seed(0) np.random.seed(0) # Ensures events don't start a few ms in the past start += timedelta(seconds=1) pareto_alpha = 0.5 pareto_mode = 5 time_passed = timedelta() while start + time_passed < end: duration = timedelta(seconds=np.random.pareto(pareto_alpha) * pareto_mode) duration = min([timedelta(hours=1), duration]) timestamp = start + time_passed time_passed += duration if start + time_passed > end: break data = random.choices( [d[1] for d in fakedata_weights], [d[0] for d in fakedata_weights] )[0] if data: yield Event(timestamp=timestamp, duration=duration, data=data)
def main() -> None: dbfile = _get_db_path() print(f"Reading from database file at {dbfile}") # Open the database conn = sqlite3.connect(_get_db_path()) # Set journal mode to WAL. conn.execute("pragma journal_mode=wal;") cur = conn.cursor() rows = list(cur.execute(query)) # TODO: Handle timezone. Maybe not needed if everything is in UTC anyway? events = [ Event( timestamp=row[4], duration=datetime.fromisoformat(row[5]) - datetime.fromisoformat(row[4]), data={ "app": row[0], "category": row[-1] }, ) for row in rows ] # for e, r in zip(events, rows): # print(e.timestamp) # print(f" - duration: {e.duration}") # print(f" - data: {e.data}") # print(f" - raw row: {r}") send_to_activitywatch(events)
def test_qslang_unknown_dose(): events = [ Event(timestamp=now, data={ "substance": "Caffeine", "amount": "?g" }), Event(timestamp=now, data={ "substance": "Caffeine", "amount": "100mg" }), Event(timestamp=now, data={ "substance": "Caffeine", "amount": "200mg" }), ] df = load_df(events) assert 0.00015 == df.iloc[0]["dose"]
def test_split_event(): now = datetime(2018, 1, 1, 0, 0).astimezone(timezone.utc) td1h = timedelta(hours=1) e = Event(timestamp=now, duration=2 * td1h, data={}) e1, e2 = _split_event(e, now + td1h) assert e1.timestamp == now assert e1.duration == td1h assert e2.timestamp == now + td1h assert e2.duration == td1h
def test_qslang_unknown_dose(): from quantifiedme.qslang import load_df, to_series events = [ Event(timestamp=now, data={ "substance": "Caffeine", "amount": "?g" }), Event(timestamp=now, data={ "substance": "Caffeine", "amount": "100mg" }), Event(timestamp=now, data={ "substance": "Caffeine", "amount": "200mg" }), ] df = load_df(events) assert 0.00015 == df.iloc[0]["dose"]
def _load_toggl(start: datetime, stop: datetime) -> List[Event]: # [x] TODO: For some reason this doesn't get all history, consider just switching back to loading from export (at least for older events) # The maintainer of togglcli fixed it quickly, huge thanks! https://github.com/AuHau/toggl-cli/issues/87 def entries_from_all_workspaces() -> List[dict]: # FIXME: Returns entries for all users # FIXME: togglcli returns the same workspace for all entries workspaces = list(api.Workspace.objects.all()) print(workspaces) print([w.id for w in workspaces]) print(f"Found {len(workspaces)} workspaces: {list(w.name for w in workspaces)}") entries: List[dict] = [ e.to_dict() for workspace in workspaces for e in api.TimeEntry.objects.all_from_reports( start=start, stop=stop, workspace=workspace.id ) ] for e in entries[-10:]: print(e) # print(e["workspace"], e["project"]) return entries def entries_from_main_workspace() -> List[dict]: entries = list(api.TimeEntry.objects.all_from_reports(start=start, stop=stop)) return [e.to_dict() for e in entries] entries = entries_from_all_workspaces() print(f"Found {len(entries)} time entries in Toggl") events_toggl = [] for e in entries: if e["start"] < start.astimezone(timezone.utc): continue project = e["project"].name if e["project"] else "no project" workspace = e["workspace"].name try: client = e["project"].client.name except AttributeError: client = "no client" description = e["description"] events_toggl.append( Event( timestamp=e["start"].isoformat(), duration=e["duration"], data={ "app": project, "title": f"{client or 'no client'} -> {project or 'no project'} -> {description or 'no description'}", "workspace": workspace, "$source": "toggl", }, ) ) return sorted(events_toggl, key=lambda e: e.timestamp)
def generous_approx(events: List[dict], max_break: float) -> timedelta: """ Returns a generous approximation of worked time by including non-categorized time when shorter than a specific duration max_break: Max time (in seconds) to flood when there's an empty slot between events """ events_e: List[Event] = [Event(**e) for e in events] return sum( map(lambda e: e.duration, flood(events_e, max_break)), timedelta(), )
def split_event_on_time(event: Event, timestamp: datetime) -> Tuple[Event, Event]: event1 = Event(**event) event2 = Event(**event) assert timestamp > event.timestamp event1.duration = timestamp - event1.timestamp event2.duration = (event2.timestamp + event2.duration) - timestamp event2.timestamp = timestamp assert event1.timestamp < event2.timestamp assert event.duration == event1.duration + event2.duration return event1, event2
def effectspan_substance(doses: list[tuple[datetime, Dose]]) -> list[Event]: """ Given a list of doses for a particular substance, return a list of events spanning the time during which the substance was active (according to durations specified in a dictionary). """ subst = doses[0][1].substance.lower() # TODO: Incorporate time-until-effect into the calculation # assert all doses of same substance assert all(dose.substance.lower() == subst for (_, dose) in doses) # assert we have duration data for the substance assert subst in subst_durations # sort doses = sorted(doses, key=lambda x: x[0]) # compute effectspan for each dose, merge overlaps events: list[Event] = [] for dt, dose in doses: end = dt + subst_durations[subst] # checks if last event overlaps with dose, if so, extend it if len(events) > 0: last_event = events[-1] # if last event ends before dose starts if (last_event.timestamp + last_event.duration) > dt: # events overlap last_event.duration = end - last_event.timestamp last_event.data["doses"].append(dose) continue e = Event( timestamp=dt, duration=subst_durations[subst], data={ "substance": subst, "doses": [dose] }, ) events.append(e) return events
def main(testing: bool): logging.basicConfig(level=logging.INFO) logger.info("Starting watcher...") client = aw_client.ActivityWatchClient("aw-watcher-input", testing=testing) client.connect() # Create bucjet bucket_name = "{}_{}".format(client.client_name, client.client_hostname) eventtype = "os.hid.input" client.create_bucket(bucket_name, eventtype, queued=False) poll_time = 1 keyboard = KeyboardListener() keyboard.start() mouse = MouseListener() mouse.start() now = datetime.now(tz=timezone.utc) while True: last_run = now sleep(poll_time) now = datetime.now(tz=timezone.utc) # If input: Send a heartbeat with data, ensure the span is correctly set, and don't use pulsetime. # If no input: Send a heartbeat with all-zeroes in the data, use a pulsetime. # FIXME: Doesn't account for scrolling # FIXME: Counts both keyup and keydown keyboard_data = keyboard.next_event() mouse_data = mouse.next_event() merged_data = dict(**keyboard_data, **mouse_data) e = Event(timestamp=last_run, duration=(now - last_run), data=merged_data) pulsetime = 0.0 if all(map(lambda v: v == 0, merged_data.values())): pulsetime = poll_time + 0.1 logger.info("No new input") else: logger.info(f"New input: {e}") client.heartbeat(bucket_name, e, pulsetime=pulsetime, queued=True)
def get_events(): """ Retrieves AFK-filtered events, only returns events which are Uncategorized. """ start = datetime(2022, 1, 1, tzinfo=timezone.utc) now = datetime.now(tz=timezone.utc) timeperiods = [(start, now)] # TODO: Use tools in aw-research to load categories from toml file categories = [ ( ["Work"], {"type": "regex", "regex": "aw-|activitywatch", "ignore_case": True}, ), ] canonicalQuery = queries.canonicalEvents( queries.DesktopQueryParams( bid_window="aw-watcher-window_", bid_afk="aw-watcher-afk_", classes=categories, ) ) res = awc.query( f""" {canonicalQuery} events = filter_keyvals(events, "$category", [["Uncategorized"]]); duration = sum_durations(events); RETURN = {{"events": events, "duration": duration}}; """, timeperiods, ) events = res[0]["events"] print(f"Fetched {len(events)} events") return [Event(**e) for e in events]
def load_toggl(start: datetime, stop: datetime) -> List[Event]: # [x] TODO: For some reason this doesn't get all history, consider just switching back to loading from export (at least for older events) # The maintainer of togglcli fixed it quickly, huge thanks! https://github.com/AuHau/toggl-cli/issues/87 def entries_from_all_workspaces(): # [ ] TODO: Several issues, such as not setting the user of each TimeEntry and setting the same workspace on every TimeEntry workspaces = list(api.Workspace.objects.all()) print(f'Found {len(workspaces)} workspaces: {list(w.name for w in workspaces)}') entries = __builtins__.sum([list(api.TimeEntry.objects.all_from_reports(start=start, stop=stop, workspace=workspace)) for workspace in workspaces], []) for e in entries[-10:]: print(e['workspace'], e['project']) return [e.to_dict() for e in entries] def entries_from_main_workspace(): entries = list(api.TimeEntry.objects.all_from_reports(start=start, stop=stop)) return [e.to_dict() for e in entries] entries = entries_from_main_workspace() print(f"Found {len(entries)} time entries in Toggl") events_toggl = [] for e in entries: if e['start'] < start.astimezone(timezone.utc): continue project = e['project'].name if e['project'] else 'no project' try: client = e['project'].client.name except AttributeError: client = 'no client' description = e['description'] events_toggl.append(Event(timestamp=e['start'].isoformat(), duration=e['duration'], data={'app': project, 'title': f"{client or 'no client'} -> {project or 'no project'} -> {description or 'no description'}", '$source': 'toggl'})) return sorted(events_toggl, key=lambda e: e.timestamp)
def _tag_one(e: Event, classes: List[Tuple[Tag, Rule]]) -> Event: e.data["$tags"] = [_cls for _cls, rule in classes if rule.match(e)] return e
def _categorize_one(e: Event, classes: List[Tuple[Category, Rule]]) -> Event: e.data["$category"] = _pick_category( [_cls for _cls, rule in classes if rule.match(e)]) return e
def heartbeat(obj: _Context, bucket_id: str, data: str, pulsetime: int): now = datetime.now(timezone.utc) e = Event(duration=0, data=json.loads(data), timestamp=now) print(e) obj.client.heartbeat(bucket_id, e, pulsetime)
def query() -> List[Event]: awc = aw_client.ActivityWatchClient(testing=False) hostname = "erb-main2-arch" # Rough start of first EEG data collection start = datetime(2020, 9, 20, tzinfo=timezone.utc) stop = datetime.now(tz=timezone.utc) def cat_re(re_str): return {"type": "regex", "regex": re_str} # Basic set of categories to use as labels # TODO: Add assert to ensure all categories have matching events # FIXME: For some reason escape sequences don't work, might just be how strings are interpolated into the query. categories = [ [["Editing"], cat_re("NVIM")], [["Editing", "Code"], cat_re(r"[.](py|rs|js|ts)")], [["Editing", "Prose"], cat_re(r"[.](tex|md)")], [["Reading docs"], cat_re("readthedocs.io")], [["Stack Overflow"], cat_re("Stack Overflow")], [["GitHub", "Pull request"], cat_re(r"Pull Request #[0-9]+")], [["GitHub", "Issues"], cat_re(r"Issue #[0-9]+")], # NOTE: There may be a significant difference between scrolling on the landing page and actually watching videos [["YouTube"], cat_re("YouTube")], [["Twitter"], cat_re("Twitter")], [["Markets"], cat_re("tradingview.com")], ] query = """ events = flood(query_bucket("aw-watcher-window_{hostname}")); not_afk = flood(query_bucket("aw-watcher-afk_{hostname}")); not_afk = filter_keyvals(not_afk, "status", ["not-afk"]); events = filter_period_intersect(events, not_afk); events = categorize(events, {categories}); cat_events = sort_by_duration(merge_events_by_keys(events, ["$category"])); RETURN = { "events": events, "duration_by_cat": cat_events }; """ # Insert parameters # (not done with f-strings since they don't like when there's other {...}'s in the string, and I don't want to {{...}}) query = query.replace("{hostname}", hostname) query = query.replace("{categories}", json.dumps(categories)) print("Querying aw-server...") data = awc.query(query, [(start, stop)]) # Since we're only querying one timeperiod result: dict = data[0] # pprint(result, depth=1) # pprint(result["events"][0]) # Transform to Event events = [Event(**e) for e in result["events"]] duration_by_cat = [Event(**e) for e in result["duration_by_cat"]] print("Time by category:") print_events(duration_by_cat, lambda e: e["data"]["$category"]) return events
def _parse_events(events: List[dict]) -> List[Event]: return [Event(**event) for event in events]
def main(): now = datetime.now(timezone.utc) td1day = timedelta(days=1) td1yr = timedelta(days=365) parser = argparse.ArgumentParser( prog="aw-cli", description='A CLI utility for interacting with ActivityWatch.') parser.set_defaults(which='none') parser.add_argument('--host', default="127.0.0.1:5600", help="Host to use, in the format HOSTNAME[:PORT]") parser.add_argument('--testing', action='store_true', help="Set to use testing ports by default") subparsers = parser.add_subparsers(help='sub-command help') parser_heartbeat = subparsers.add_parser( 'heartbeat', help='Send a heartbeat to the server') parser_heartbeat.set_defaults(which='heartbeat') parser_heartbeat.add_argument('--pulsetime', default=60, help='Pulsetime to use') parser_heartbeat.add_argument('bucket', help='bucketname to send heartbeat to') parser_heartbeat.add_argument('data', default="{}", help='JSON data to send in heartbeat') parser_buckets = subparsers.add_parser('buckets', help='List all buckets') parser_buckets.set_defaults(which='buckets') parser_buckets = subparsers.add_parser('events', help='Query events from bucket') parser_buckets.set_defaults(which='events') parser_buckets.add_argument('bucket') parser_query = subparsers.add_parser('query', help='Query events from bucket') parser_query.set_defaults(which='query') parser_query.add_argument('path') parser_query.add_argument('--name') parser_query.add_argument('--cache', action='store_true') parser_query.add_argument('--json', action='store_true', help='Output resulting JSON') parser_query.add_argument('--start', default=now - td1day, type=_valid_date) parser_query.add_argument('--end', default=now + 10 * td1yr, type=_valid_date) args = parser.parse_args() # print("Args: {}".format(args)) client = aw_client.ActivityWatchClient( host=args.host.split(':')[0], port=int((args.host.split(':')[1:] + [5600 if not args.testing else 5666]).pop())) if args.which == "heartbeat": e = Event(duration=0, data=json.loads(args.data), timestamp=now) print(e) client.heartbeat(args.bucket, e, args.pulsetime) elif args.which == "buckets": buckets = client.get_buckets() print("Buckets:") for bucket in buckets: print(" - {}".format(bucket)) elif args.which == "events": events = client.get_events(args.bucket) print("events:") for e in events: print(" - {} ({}) {}".format( e.timestamp.replace(tzinfo=None, microsecond=0), str(e.duration).split(".")[0], e.data)) elif args.which == "query": with open(args.path) as f: query = f.read() result = client.query(query, args.start, args.end, cache=args.cache, name=args.name) if args.json: print(json.dumps(result)) else: for period in result: print("Showing 10 out of {} events:".format(len(period))) for event in period[:10]: event.pop("id") event.pop("timestamp") print(" - Duration: {} \tData: {}".format( str(timedelta( seconds=event["duration"])).split(".")[0], event["data"])) print("Total duration:\t", timedelta(seconds=sum(e["duration"] for e in period))) else: parser.print_help()