From 76b2dd4408449443ad5eaf9f967129bf8cd2faa1 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Fri, 3 Dec 2021 16:22:54 +0100 Subject: [PATCH] Make nomie dataclasses immutable --- habitmove/loopdata.py | 12 ++++- habitmove/nomie.py | 46 +++++++++------- habitmove/nomiedata.py | 44 ++++++++-------- habitmove/repetitions.py | 111 +++++++++++++++------------------------ 4 files changed, 102 insertions(+), 111 deletions(-) diff --git a/habitmove/loopdata.py b/habitmove/loopdata.py index 8a10b3f..3c97494 100644 --- a/habitmove/loopdata.py +++ b/habitmove/loopdata.py @@ -2,7 +2,8 @@ from typing import Optional from dataclasses import dataclass from uuid import uuid4 -# A loop Habit representation. Tracks anything whose value can be encapsulated in a numerical value. +# A loop Habit representation. +# Tracks anything whose value can be encapsulated in a numerical value. @dataclass class Habit: name: str @@ -23,3 +24,12 @@ class Habit: def __post_init__(self): if not self.uuid: self.uuid = uuid4().hex + + +# A Loop repetition representation, containing only the bare minimum +# for its successful entry into the Loop Habit Tracker database. +@dataclass +class Repetition: + habit_uuid: str + timestamp: int + value: Optional[int] diff --git a/habitmove/nomie.py b/habitmove/nomie.py index f34f3e0..4764272 100644 --- a/habitmove/nomie.py +++ b/habitmove/nomie.py @@ -31,10 +31,14 @@ def verify_continue(data: NomieImport): trackers += t.label + ", " trackers = trackers[:-2] + activity_count = 0 + for e in data.events: + activity_count += len(e.activities) if e.activities else 0 + print(f"Exporting from nomie {data.version}:") print(f"Found trackers: {trackers}") print( - f"Found events: {len(data.events)} entries, containing {len(data.activities)} individual activities." + f"Found events: {len(data.events)} entries, containing {activity_count} individual activities." ) if not confirmation_question("Do you want to continue?", default_no=False): print("Aborted.") @@ -48,15 +52,19 @@ def get_trackers(raw_trackers): return tracker_list -def get_notes(raw_notes): - notes = list[Event]() - for note in raw_notes: - note["id"] = note["_id"] - note.pop("_id") - note["text"] = note["note"] - note.pop("note") - notes.append(Event(**note)) - return notes +def get_events(raw_events, tracker_list): + events = list[Event]() + for event in raw_events: + event["id"] = event["_id"] + event.pop("_id") + event["text"] = event["note"] + event.pop("note") + + activities = get_activities_for_event(event["text"], tracker_list) + + events.append(Event(**event, activities=activities)) + + return events def extract_tags_from_text(text, tagmarker="#"): @@ -74,14 +82,13 @@ def extract_tags_from_text(text, tagmarker="#"): return tags_with_int_counters -def get_activities(tracker_list, note_list): +def get_activities_for_event(event_text, tracker_list): activities = [] - for note in note_list: - tag_list = extract_tags_from_text(note.text) - for tracker in tracker_list: - for tag in tag_list: - if tracker.tag in tag[0]: - activities.append(Activity(tracker=tracker, value=tag[1])) + tag_list = extract_tags_from_text(event_text) + for tracker in tracker_list: + for tag in tag_list: + if tracker.tag in tag[0]: + activities.append(Activity(tracker=tracker, value=tag[1])) return activities @@ -91,10 +98,9 @@ def get_data(file, interactive=True): nomie_version = raw_data["nomie"]["number"] tracker_list = get_trackers(raw_data["trackers"]) - note_list = get_notes(raw_data["events"]) - activity_list = get_activities(tracker_list, note_list) + event_list = get_events(raw_data["events"], tracker_list) - data = NomieImport(nomie_version, tracker_list, note_list, activity_list) + data = NomieImport(nomie_version, tracker_list, event_list) if interactive: verify_continue(data) diff --git a/habitmove/nomiedata.py b/habitmove/nomiedata.py index e9bac95..d450b2c 100644 --- a/habitmove/nomiedata.py +++ b/habitmove/nomiedata.py @@ -2,7 +2,7 @@ from typing import Optional, Any from dataclasses import dataclass, field # A nomie habit tracker. Tracks anything whose value can be encapsulated in a numerical value. -@dataclass +@dataclass(frozen=True) class Tracker: color: str emoji: str @@ -26,33 +26,33 @@ class Tracker: score_calc: Optional[list[dict[str, Any]]] = field(default_factory=lambda: []) -# A nomie note. Records any circumstance of 'something happened' through prose. -# These are undigested events, whose changed trackers are still encapsulated -# in the 'note' field as continuous text. -@dataclass -class Event: - id: str - text: str - start: int - end: int - score: int - lat: float - lng: float - modified: Optional[bool] = False - source: Optional[str] = "n5" - offset: Optional[str] = "" - location: Optional[str] = "" - - -@dataclass +@dataclass(frozen=True) class Activity: tracker: Tracker value: Optional[int] = 1 -@dataclass +# A nomie note. Records any circumstance of 'something happened' through prose. +# These are undigested events, whose changed trackers are still encapsulated +# in the 'note' field as continuous text. +@dataclass(frozen=True) +class Event: + id: str + 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 + + +@dataclass(frozen=True) class NomieImport: version: str trackers: list[Tracker] events: list[Event] - activities: list[Activity] diff --git a/habitmove/repetitions.py b/habitmove/repetitions.py index d04f1e5..1fb7ab2 100644 --- a/habitmove/repetitions.py +++ b/habitmove/repetitions.py @@ -1,5 +1,6 @@ import sqlite3 -import re +from datetime import datetime +from habitmove.loopdata import Habit, Repetition def migrate(db, habitlist, events): @@ -7,9 +8,7 @@ def migrate(db, habitlist, events): habits = habit_list_add_ids(c, habitlist) repetitions = get_all_repetitions(habits, events) for rep in repetitions: - add_to_database( - c, rep["id"], rep["timestamp"], 2 if "value" not in rep else rep["value"] - ) + add_to_database(c, habits, rep) LOOP_RANGE_VALUE_MULTIPLIER = 1000 @@ -26,55 +25,20 @@ def get_all_repetitions(habits, events): """ repetitions = [] for event in events: - for habit_id in habits.keys(): - reps = tags_to_repetitions( - habit_id, habits[habit_id], extract_tags(event.text), event.end - ) - if reps: - repetitions.extend(reps) + for activity in event.activities: + for habit in habits.values(): + # TODO Fix reaching a layer too far into activity -> tracker + if habit.uuid == activity.tracker.id: + rep = Repetition( + habit_uuid=habit.uuid, timestamp=event.end, value=2 + ) + if habit.type == 1 and activity.value: + rep.value = activity.value * LOOP_RANGE_VALUE_MULTIPLIER + repetitions.append(rep) return repetitions -def extract_tags(text, tagmarker="#"): - """Return lists of tuples of all event tags found in text. - Parameters: - text (str): The text to search through. - tagmarker (str): Optional character marking beginning of tag, defaults to '#'. - Returns: - tags (list): List of tuples in the form [('tag', '3'), ('anothertag', '')]. - """ - string_tags = re.findall(rf"{tagmarker}(\w+)(?:\((\d+)\))?", text) - tags_with_int_counters = [] - for tag in string_tags: - tags_with_int_counters.append((tag[0], None if tag[1] == "" else int(tag[1]))) - return tags_with_int_counters - - -# does not do: -# non-range habits but still #habit(3) number included, adding multiple? -def tags_to_repetitions(habit_id, habit, tags, timestamp): - """Return a list of all repetitions generated from the tags and habits passed in. - Parameters: - habits (list): Collection of habits, with minimum necessary fields description and id. - tags (list): Collection of tag tuples. - Returns: - repetitions (list): Collection of habits for which a corresponding tag has - been found. If they correspond that means at the timestamp - the habit has been checked in, and a repetition is created. - Contains fields id, timestamp, value (for ranges). - """ - reps = [] - for tag in tags: - if habit.description in tag[0]: - repetition = {"id": habit_id, "timestamp": timestamp} - if tag[1]: - if habit.type == 1: - repetition["value"] = tag[1] * LOOP_RANGE_VALUE_MULTIPLIER - reps.append(repetition) - return reps - - # TODO possibly just get rid of this entirely def habit_list_add_ids(c, habitlist): """Return the collection of habits with their sqlite id added. @@ -93,30 +57,41 @@ def habit_list_add_ids(c, habitlist): return with_id -def fetch_habit_id(c, uuid): +def fetch_habit_id(cursor: sqlite3.Cursor, uuid: str): """Return sqlite internal id for habit with uuid. Parameters: - c (sqlite.db.cursor): SQL cursor of database to query. - uuid (str): Unique id of habit to query for. + :param c: SQL cursor of database to query. + :param uuid: Unique id of habit to query for. Returns: - id (int): SQLite internal id for habit queried for. + :return id: SQLite internal id for habit queried for. """ - c.execute("select id from Habits where uuid = ?", ([uuid])) - id = c.fetchone() + cursor.execute("select id from Habits where uuid = ?", ([uuid])) + id = cursor.fetchone() if id is not None: return id[0] -def add_to_database(cursor, habit_id, timestamp, value=2): - try: - cursor.execute( - """ - INSERT INTO - Repetitions(id, habit, timestamp, value) - VALUES (NULL, ?, ?, ?) - """, - (habit_id, timestamp, value), - ) - except sqlite3.IntegrityError: - # TODO better error handling - print(f"fail to register {habit_id}: timestamp {timestamp} not unique") +def add_to_database( + cursor: sqlite3.Cursor, habits: dict[int, Habit], repetition: Repetition +): + """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. + """ + for sql_id, habit in habits.items(): + if repetition.habit_uuid == habit.uuid: + try: + cursor.execute( + """ + INSERT INTO + Repetitions(id, habit, timestamp, value) + VALUES (NULL, ?, ?, ?) + """, + (sql_id, repetition.timestamp, repetition.value), + ) + except sqlite3.IntegrityError: + # TODO better error handling + print( + f"{sql_id}, {habit.name}: timestamp {datetime.fromtimestamp(repetition.timestamp/1000)} not unique, moving timestamp slightly." + )