diff --git a/CHANGELOG.md b/CHANGELOG.md index 70c6f72..cfe3032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project tries to adhere to [Semantic Versioning](https://semver.org/spe ## [Unreleased] +### Added + +* Added import of duplicate activities + In nomie, you can have multiple activity tags in a single event, + e.g. #smoking #smoking. This does not work in Loop, but we import + them as separate counter instances instead. + +### Fixed + +* Create missing PRAGMA values for Loop SQLite database, fixing failing import + ## [0.3.1] - 2021-09-30 ### Fixed diff --git a/README.md b/README.md index 66175ad..87ac996 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,44 @@ poetry install poetry run habitmove ``` +## Roadmap + +* [x] transform nomie data into dataclasses + * [x] Trackers + * [x] Events + * [x] Activities +* [x] transform loop data into dataclasses + * [x] create habit + * [x] create some way of getting 'length' of habit (how many fields) + * [x] move extracting of 'activity' from loop to nomie functionality (coupled with nomie note representation) + * [x] make loop migration use this activity data + * [x] create repetition +* [ ] clean up README +* [ ] begin adding tests + * [ ] add some unit tests for various functions + * [ ] and at least an integration test for the stable database (loop-2021-12-02.db or equivalent) +* [ ] abstract migration target away from loop +* [ ] abstract import away from nomie +* [ ] abstract importer/migrator themselves to work with other targets +* [ ] allow migration to/from nomie/loop + +## Internal concepts + +The migrator has the concept of a tracker, an event, and an activity: + +* A tracker is the system / the goal / the accumulator that tracks a certain number of you 'doing' things. + * In Loop Habit Tracker it corresponds to a Habit +* An event is something happening, something being emitted but generally in its raw form. They do *not* + exist for pure numerical value tracking. + * Nomie has the idea of an event as a little text note that you write + * Loop Habit Tracker has no equivalent of an event and they will simply be discarded + * Generally, this will be most useful if you wish to preserve and keep prose notes in the system +* An activity is the interpreted variation of an event and which only effects your trackers. + Essentially something that changes a tracker's counter / value, like you doing sports or drinking water. + * Nomie keeps activities within its notes (as tags) and has no separate concept of them + * Loop Habit Tracker is only geared toward tracking activities and they are its main building block + * Generally, this will be quantifiable data and most useful for statistics and similar analysis + # Schemas A collection of the schemas of Loop habit sqlite database for later migration diff --git a/habitmove/loopdata.py b/habitmove/loopdata.py index 3c97494..5d91a99 100644 --- a/habitmove/loopdata.py +++ b/habitmove/loopdata.py @@ -7,22 +7,22 @@ from uuid import uuid4 @dataclass class Habit: name: str - type: Optional[int] = 0 # 1 is range counter - archived: Optional[int] = 0 - color: Optional[int] = 0 - highlight: Optional[int] = 0 - freq_den: Optional[int] = 1 - freq_num: Optional[int] = 1 - target_value: Optional[int] = 0 - description: Optional[str] = "" - question: Optional[str] = "" - unit: Optional[str] = "" - position: Optional[int] = 0 - uuid: Optional[str] = None + type: int = 0 # 1 is range counter + archived: int = 0 + color: int = 0 + highlight: int = 0 + freq_den: int = 1 + freq_num: int = 1 + target_value: int = 0 + description: str = "" + question: str = "" + unit: str = "" + position: int = 0 + uuid: str = "" # TODO test post init uuid setting def __post_init__(self): - if not self.uuid: + if not self.uuid or self.uuid == "": self.uuid = uuid4().hex diff --git a/habitmove/nomiedata.py b/habitmove/nomiedata.py index d450b2c..cbcb9c7 100644 --- a/habitmove/nomiedata.py +++ b/habitmove/nomiedata.py @@ -17,19 +17,19 @@ class Tracker: uom: str # TODO no idea what include does include: str - min: Optional[int] = 0 - max: Optional[int] = 0 - goal: Optional[int] = 0 - default: Optional[int] = 0 + min: int = 0 + max: int = 0 + goal: int = 0 + default: int = 0 # TODO score can be string (if custom) or int (if simple good/bad) - score: Optional[str] = "" - score_calc: Optional[list[dict[str, Any]]] = field(default_factory=lambda: []) + score: str = "" + score_calc: list[dict[str, Any]] = field(default_factory=lambda: []) @dataclass(frozen=True) class Activity: tracker: Tracker - value: Optional[int] = 1 + value: int = 1 # A nomie note. Records any circumstance of 'something happened' through prose. @@ -41,14 +41,14 @@ class Event: start: int end: int text: str - activities: Optional[list[Activity]] = field(default_factory=lambda: []) - score: Optional[int] = 0 - lat: Optional[float] = 0.0 - lng: Optional[float] = 0.0 - location: Optional[str] = "" - modified: Optional[bool] = False - offset: Optional[str] = "" # local timezone offset? - source: Optional[str] = "n5" # nomie version + activities: list[Activity] = field(default_factory=lambda: []) + score: int = 0 + lat: float = 0.0 + lng: float = 0.0 + location: str = "" + modified: bool = False + offset: str = "" # local timezone offset? + source: str = "n5" # nomie version @dataclass(frozen=True) diff --git a/habitmove/repetitions.py b/habitmove/repetitions.py index 1fb7ab2..bfd21ba 100644 --- a/habitmove/repetitions.py +++ b/habitmove/repetitions.py @@ -1,27 +1,33 @@ import sqlite3 +from typing import Optional from datetime import datetime from habitmove.loopdata import Habit, Repetition +from habitmove.nomiedata import Event -def migrate(db, habitlist, events): +def migrate(db: sqlite3.Connection, habits: list[Habit], events: list[Event]) -> None: + """Move Loop Activities contained in all Events matching Habits passed in into database. + :param db: Database to populate. + :param habits: List of Habits to find matching repetitions for. + :param events: List of events to find repetitions in. + """ c = db.cursor() - habits = habit_list_add_ids(c, habitlist) - repetitions = get_all_repetitions(habits, events) + habits_with_sql_id = habit_list_add_ids(c, habits) + repetitions = get_all_repetitions(habits_with_sql_id, events) for rep in repetitions: - add_to_database(c, habits, rep) + add_to_database(c, habits_with_sql_id, rep) LOOP_RANGE_VALUE_MULTIPLIER = 1000 -def get_all_repetitions(habits, events): - """Return list of all repetitions found of habits in events passed in. - Parameters: - habits (list): Collection of habits, with minimum necessary fields description and id. - events (list): Collection of events, with minimum necessary field end. - Returns: - repetitions (list): Collection of events transformed into Loop repetitions. - Contains fields id, timestamp, value (for ranges). +def get_all_repetitions( + habits: dict[int, Habit], events: list[Event] +) -> list[Repetition]: + """Return list of all repetitions found in events passed in. + :param habits: Dict of habits with sql_ids that repetitions can be for. + :param events: List of events to search through for repetitions. + :return repetitions: List of Loop repetitions. """ repetitions = [] for event in events: @@ -40,29 +46,25 @@ def get_all_repetitions(habits, events): # TODO possibly just get rid of this entirely -def habit_list_add_ids(c, habitlist): +def habit_list_add_ids(c: sqlite3.Cursor, habitlist: list[Habit]) -> dict[int, Habit]: """Return the collection of habits with their sqlite id added. - Parameters: - c (sqlite.db.cursor): SQL cursor of database to query. - habitlist (list[Habit]): Full habit collection to return a simplified view of. - Returns: - habit_id_dict (dict[Habit]): The habit collection as a dict with the keys + :param c: SQL cursor of database to query. + :param habitlist: Habits to get sql IDs for. + :return habit_id_dict: The habit collection as a dict with the keys consisting of the habit's sqlite database ID. """ with_id = {} for h in habitlist: - sql_id = fetch_habit_id(c, h.uuid) + sql_id = fetch_habit_id(c, h.uuid or "") with_id[sql_id] = h return with_id -def fetch_habit_id(cursor: sqlite3.Cursor, uuid: str): +def fetch_habit_id(cursor: sqlite3.Cursor, uuid: str) -> Optional[int]: """Return sqlite internal id for habit with uuid. - Parameters: :param c: SQL cursor of database to query. :param uuid: Unique id of habit to query for. - Returns: :return id: SQLite internal id for habit queried for. """ cursor.execute("select id from Habits where uuid = ?", ([uuid])) @@ -73,9 +75,8 @@ def fetch_habit_id(cursor: sqlite3.Cursor, uuid: str): def add_to_database( cursor: sqlite3.Cursor, habits: dict[int, Habit], repetition: Repetition -): +) -> None: """Insert the repetition into a sqlite3 table suitable for Loop. - Parameters: :param c: SQL cursor of database to query. :sql_id: Internal sqlite database id of the habit the repetition belongs to. """ @@ -95,3 +96,12 @@ def add_to_database( print( f"{sql_id}, {habit.name}: timestamp {datetime.fromtimestamp(repetition.timestamp/1000)} not unique, moving timestamp slightly." ) + add_to_database( + cursor, + habits, + Repetition( + repetition.habit_uuid, + repetition.timestamp + 1, + repetition.value, + ), + )