示例#1
0
    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)
示例#2
0
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 []
示例#3
0
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)
示例#6
0
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)
示例#7
0
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)
示例#8
0
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"]
示例#9
0
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
示例#10
0
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"]
示例#11
0
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)
示例#12
0
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(),
    )
示例#13
0
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
示例#14
0
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
示例#15
0
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]
示例#17
0
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)
示例#18
0
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
示例#19
0
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
示例#20
0
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)
示例#21
0
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
示例#22
0
def _parse_events(events: List[dict]) -> List[Event]:
    return [Event(**event) for event in events]
示例#23
0
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()