diff --git a/aw_core/models.py b/aw_core/models.py index 9fb9344..d85d0ae 100644 --- a/aw_core/models.py +++ b/aw_core/models.py @@ -3,7 +3,7 @@ import numbers import typing from datetime import datetime, timedelta, timezone -from typing import Any, List, Dict, Union, Optional +from typing import Any, List, Dict, Union, Optional, Text import iso8601 @@ -139,3 +139,51 @@ def duration(self, duration: Duration) -> None: raise TypeError( "Couldn't parse duration of invalid type {}".format(type(duration)) ) + + +class Setting(dict): + """ + Used to represents a setting. + """ + + def __init__( + self, + key: Text = None, + value: Text = None, + ) -> None: + self.key = key + self.value = value + + def __eq__(self, other: object) -> bool: + return True + + def to_json_dict(self) -> dict: + """Useful when sending data over the wire. + Any mongodb interop should not use do this as it accepts datetimes.""" + json_data = self.copy() + return json_data + + def to_json_str(self) -> str: + data = self.to_json_dict() + return json.dumps(data) + + def _hasprop(self, propname: str) -> bool: + """Badly named, but basically checks if the underlying + dict has a prop, and if it is a non-empty list""" + return propname in self and self[propname] is not None + + @property + def key(self) -> Text: + return self["key"] if self._hasprop("key") else None + + @key.setter + def key(self, key: Text) -> None: + self["key"] = key + + @property + def value(self) -> dict: + return self["value"] if self._hasprop("value") else None + + @value.setter + def value(self, value: Text) -> None: + self["value"] = value diff --git a/aw_core/schemas/settings.json b/aw_core/schemas/settings.json new file mode 100644 index 0000000..ec5791d --- /dev/null +++ b/aw_core/schemas/settings.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Settings", + "description": "The Settings model that is used in ActivityWatch", + "type": "object", + "required": ["key", "value"], + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + } +} diff --git a/aw_datastore/datastore.py b/aw_datastore/datastore.py index 9e7ebae..9792a67 100644 --- a/aw_datastore/datastore.py +++ b/aw_datastore/datastore.py @@ -67,6 +67,12 @@ def delete_bucket(self, bucket_id: str): def buckets(self): return self.storage_strategy.buckets() + def get_settings(self): + return self.storage_strategy.get_settings() + + def update_setting(self, key, value): + return self.storage_strategy.update_setting(key, value) + class Bucket: def __init__(self, datastore: Datastore, bucket_id: str) -> None: diff --git a/aw_datastore/storages/abstract.py b/aw_datastore/storages/abstract.py index 6755c0d..5799495 100644 --- a/aw_datastore/storages/abstract.py +++ b/aw_datastore/storages/abstract.py @@ -3,7 +3,7 @@ from datetime import datetime from abc import ABCMeta, abstractmethod, abstractproperty -from aw_core.models import Event +from aw_core.models import Event, Setting class AbstractStorage(metaclass=ABCMeta): @@ -79,3 +79,11 @@ def replace(self, bucket_id: str, event_id: int, event: Event) -> bool: @abstractmethod def replace_last(self, bucket_id: str, event: Event) -> None: raise NotImplementedError + + @abstractmethod + def get_settings(self) -> List[Setting]: + raise NotImplementedError + + @abstractmethod + def update_setting(self, key: str, value: str) -> bool: + raise NotImplementedError diff --git a/aw_datastore/storages/memory.py b/aw_datastore/storages/memory.py index 08b58aa..af2f09a 100644 --- a/aw_datastore/storages/memory.py +++ b/aw_datastore/storages/memory.py @@ -114,3 +114,11 @@ def replace(self, bucket_id, event_id, event): def replace_last(self, bucket_id, event): self.db[bucket_id][-1] = event + + def get_settings(self): + settings = self.db["settings"] + return settings + + def update_setting(self, key: str, value: str): + self.db["settings"][key] = value + return True diff --git a/aw_datastore/storages/mongodb.py b/aw_datastore/storages/mongodb.py index 3c27655..54a5683 100644 --- a/aw_datastore/storages/mongodb.py +++ b/aw_datastore/storages/mongodb.py @@ -166,3 +166,11 @@ def replace(self, bucket_id: str, event_id, event: Event) -> bool: ) event.id = event_id return True + + def get_settings(self): + settings = self.db["settings"].find() + return settings + + def update_setting(self, key: str, value: str): + self.db["settings"].update({"key": key}, {"value": value}, True ) + return True diff --git a/aw_datastore/storages/peewee.py b/aw_datastore/storages/peewee.py index 6f93ca8..83efe9d 100644 --- a/aw_datastore/storages/peewee.py +++ b/aw_datastore/storages/peewee.py @@ -17,7 +17,7 @@ ) from playhouse.sqlite_ext import SqliteExtDatabase -from aw_core.models import Event +from aw_core.models import Event, Setting from aw_core.dirs import get_data_dir from .abstract import AbstractStorage @@ -107,6 +107,18 @@ def json(self): } +class SettingModel(BaseModel): + id = AutoField() + key = CharField(unique=True) + value = CharField() + + def json(self): + return { + "key": self.key, + "value": self.value, + } + + class PeeweeStorage(AbstractStorage): sid = "peewee" @@ -130,6 +142,7 @@ def __init__(self, testing: bool = True, filepath: str = None) -> None: self.bucket_keys: Dict[str, int] = {} BucketModel.create_table(safe=True) EventModel.create_table(safe=True) + SettingModel.create_table(safe=True) self.update_bucket_keys() def update_bucket_keys(self) -> None: @@ -327,3 +340,45 @@ def _where_range( q = q.where(EventModel.timestamp <= endtime) return q + + def get_settings( + self, + ): + """ + Fetch all settings + + Example raw query: + + SELECT * FROM settings + + """ + q = (SettingModel.select() + + res = q.execute() + settings = [Setting(**e) for e in list(map(SettingModel.json, res))] + + return settings + + def update_setting(self, key, value): + """ + Update a setting + + Example raw query: + + UPDATE settings + SET value = ? + WHERE key = ? + + """ + q = SettingModel + .insert(value= value, key= key) + .on_conflict( + conflict_target=[SettingModel.key], + preserve=[SettingModel.key], + update={SettingModel.value: value} + ) + + + q.execute() + return True + diff --git a/aw_datastore/storages/sqlite.py b/aw_datastore/storages/sqlite.py index 6029bad..a888867 100644 --- a/aw_datastore/storages/sqlite.py +++ b/aw_datastore/storages/sqlite.py @@ -13,7 +13,11 @@ logger = logging.getLogger(__name__) -LATEST_VERSION = 1 +""" +v1: initial version +v2: add settings table +""" +LATEST_VERSION = 2 # The max integer value in SQLite is signed 8 Bytes / 64 bits MAX_TIMESTAMP = 2 ** 63 - 1 @@ -42,6 +46,13 @@ ) """ +CREATE_SETTINGS_TABLE = """ + CREATE TABLE IF NOT EXISTS settings ( + key TEXT NOT NULL, + value TEXT NOT NULL + ) +""" + INDEX_BUCKETS_TABLE_ID = """ CREATE INDEX IF NOT EXISTS event_index_id ON events(id); """ @@ -77,6 +88,7 @@ def __init__(self, testing, filepath: str = None, enable_lazy_commit=True) -> No # Create tables self.conn.execute(CREATE_BUCKETS_TABLE) self.conn.execute(CREATE_EVENTS_TABLE) + self.conn.execute(CREATE_SETTINGS_TABLE) self.conn.execute(INDEX_BUCKETS_TABLE_ID) self.conn.execute(INDEX_EVENTS_TABLE_STARTTIME) self.conn.execute(INDEX_EVENTS_TABLE_ENDTIME) @@ -303,3 +315,18 @@ def get_eventcount( row = rows.fetchone() eventcount = row[0] return eventcount + + def get_settings(self): + self.commit() + c = self.conn.cursor + query = "SELECT * FROM settings" + rows = c.execute(query) + return rows + + def update_setting(self, key, value): + query = """UPDATE settings + SET value = ? + WHERE key = ?""" + self.conn.execute(query, [key, value]) + self.conditional_commit(1) + return True