Make nomie dataclasses immutable
This commit is contained in:
parent
76bedd42ee
commit
76b2dd4408
4 changed files with 102 additions and 111 deletions
|
@ -2,7 +2,8 @@ from typing import Optional
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from uuid import uuid4
|
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
|
@dataclass
|
||||||
class Habit:
|
class Habit:
|
||||||
name: str
|
name: str
|
||||||
|
@ -23,3 +24,12 @@ class Habit:
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
if not self.uuid:
|
if not self.uuid:
|
||||||
self.uuid = uuid4().hex
|
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]
|
||||||
|
|
|
@ -31,10 +31,14 @@ def verify_continue(data: NomieImport):
|
||||||
trackers += t.label + ", "
|
trackers += t.label + ", "
|
||||||
trackers = trackers[:-2]
|
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"Exporting from nomie {data.version}:")
|
||||||
print(f"Found trackers: {trackers}")
|
print(f"Found trackers: {trackers}")
|
||||||
print(
|
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):
|
if not confirmation_question("Do you want to continue?", default_no=False):
|
||||||
print("Aborted.")
|
print("Aborted.")
|
||||||
|
@ -48,15 +52,19 @@ def get_trackers(raw_trackers):
|
||||||
return tracker_list
|
return tracker_list
|
||||||
|
|
||||||
|
|
||||||
def get_notes(raw_notes):
|
def get_events(raw_events, tracker_list):
|
||||||
notes = list[Event]()
|
events = list[Event]()
|
||||||
for note in raw_notes:
|
for event in raw_events:
|
||||||
note["id"] = note["_id"]
|
event["id"] = event["_id"]
|
||||||
note.pop("_id")
|
event.pop("_id")
|
||||||
note["text"] = note["note"]
|
event["text"] = event["note"]
|
||||||
note.pop("note")
|
event.pop("note")
|
||||||
notes.append(Event(**note))
|
|
||||||
return notes
|
activities = get_activities_for_event(event["text"], tracker_list)
|
||||||
|
|
||||||
|
events.append(Event(**event, activities=activities))
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
|
|
||||||
def extract_tags_from_text(text, tagmarker="#"):
|
def extract_tags_from_text(text, tagmarker="#"):
|
||||||
|
@ -74,14 +82,13 @@ def extract_tags_from_text(text, tagmarker="#"):
|
||||||
return tags_with_int_counters
|
return tags_with_int_counters
|
||||||
|
|
||||||
|
|
||||||
def get_activities(tracker_list, note_list):
|
def get_activities_for_event(event_text, tracker_list):
|
||||||
activities = []
|
activities = []
|
||||||
for note in note_list:
|
tag_list = extract_tags_from_text(event_text)
|
||||||
tag_list = extract_tags_from_text(note.text)
|
for tracker in tracker_list:
|
||||||
for tracker in tracker_list:
|
for tag in tag_list:
|
||||||
for tag in tag_list:
|
if tracker.tag in tag[0]:
|
||||||
if tracker.tag in tag[0]:
|
activities.append(Activity(tracker=tracker, value=tag[1]))
|
||||||
activities.append(Activity(tracker=tracker, value=tag[1]))
|
|
||||||
return activities
|
return activities
|
||||||
|
|
||||||
|
|
||||||
|
@ -91,10 +98,9 @@ def get_data(file, interactive=True):
|
||||||
nomie_version = raw_data["nomie"]["number"]
|
nomie_version = raw_data["nomie"]["number"]
|
||||||
|
|
||||||
tracker_list = get_trackers(raw_data["trackers"])
|
tracker_list = get_trackers(raw_data["trackers"])
|
||||||
note_list = get_notes(raw_data["events"])
|
event_list = get_events(raw_data["events"], tracker_list)
|
||||||
activity_list = get_activities(tracker_list, note_list)
|
|
||||||
|
|
||||||
data = NomieImport(nomie_version, tracker_list, note_list, activity_list)
|
data = NomieImport(nomie_version, tracker_list, event_list)
|
||||||
if interactive:
|
if interactive:
|
||||||
verify_continue(data)
|
verify_continue(data)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ from typing import Optional, Any
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
# A nomie habit tracker. Tracks anything whose value can be encapsulated in a numerical value.
|
# A nomie habit tracker. Tracks anything whose value can be encapsulated in a numerical value.
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class Tracker:
|
class Tracker:
|
||||||
color: str
|
color: str
|
||||||
emoji: str
|
emoji: str
|
||||||
|
@ -26,33 +26,33 @@ class Tracker:
|
||||||
score_calc: Optional[list[dict[str, Any]]] = field(default_factory=lambda: [])
|
score_calc: Optional[list[dict[str, Any]]] = field(default_factory=lambda: [])
|
||||||
|
|
||||||
|
|
||||||
# A nomie note. Records any circumstance of 'something happened' through prose.
|
@dataclass(frozen=True)
|
||||||
# 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
|
|
||||||
class Activity:
|
class Activity:
|
||||||
tracker: Tracker
|
tracker: Tracker
|
||||||
value: Optional[int] = 1
|
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:
|
class NomieImport:
|
||||||
version: str
|
version: str
|
||||||
trackers: list[Tracker]
|
trackers: list[Tracker]
|
||||||
events: list[Event]
|
events: list[Event]
|
||||||
activities: list[Activity]
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import re
|
from datetime import datetime
|
||||||
|
from habitmove.loopdata import Habit, Repetition
|
||||||
|
|
||||||
|
|
||||||
def migrate(db, habitlist, events):
|
def migrate(db, habitlist, events):
|
||||||
|
@ -7,9 +8,7 @@ def migrate(db, habitlist, events):
|
||||||
habits = habit_list_add_ids(c, habitlist)
|
habits = habit_list_add_ids(c, habitlist)
|
||||||
repetitions = get_all_repetitions(habits, events)
|
repetitions = get_all_repetitions(habits, events)
|
||||||
for rep in repetitions:
|
for rep in repetitions:
|
||||||
add_to_database(
|
add_to_database(c, habits, rep)
|
||||||
c, rep["id"], rep["timestamp"], 2 if "value" not in rep else rep["value"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
LOOP_RANGE_VALUE_MULTIPLIER = 1000
|
LOOP_RANGE_VALUE_MULTIPLIER = 1000
|
||||||
|
@ -26,55 +25,20 @@ def get_all_repetitions(habits, events):
|
||||||
"""
|
"""
|
||||||
repetitions = []
|
repetitions = []
|
||||||
for event in events:
|
for event in events:
|
||||||
for habit_id in habits.keys():
|
for activity in event.activities:
|
||||||
reps = tags_to_repetitions(
|
for habit in habits.values():
|
||||||
habit_id, habits[habit_id], extract_tags(event.text), event.end
|
# TODO Fix reaching a layer too far into activity -> tracker
|
||||||
)
|
if habit.uuid == activity.tracker.id:
|
||||||
if reps:
|
rep = Repetition(
|
||||||
repetitions.extend(reps)
|
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
|
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
|
# TODO possibly just get rid of this entirely
|
||||||
def habit_list_add_ids(c, habitlist):
|
def habit_list_add_ids(c, habitlist):
|
||||||
"""Return the collection of habits with their sqlite id added.
|
"""Return the collection of habits with their sqlite id added.
|
||||||
|
@ -93,30 +57,41 @@ def habit_list_add_ids(c, habitlist):
|
||||||
return with_id
|
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.
|
"""Return sqlite internal id for habit with uuid.
|
||||||
Parameters:
|
Parameters:
|
||||||
c (sqlite.db.cursor): SQL cursor of database to query.
|
:param c: SQL cursor of database to query.
|
||||||
uuid (str): Unique id of habit to query for.
|
:param uuid: Unique id of habit to query for.
|
||||||
Returns:
|
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]))
|
cursor.execute("select id from Habits where uuid = ?", ([uuid]))
|
||||||
id = c.fetchone()
|
id = cursor.fetchone()
|
||||||
if id is not None:
|
if id is not None:
|
||||||
return id[0]
|
return id[0]
|
||||||
|
|
||||||
|
|
||||||
def add_to_database(cursor, habit_id, timestamp, value=2):
|
def add_to_database(
|
||||||
try:
|
cursor: sqlite3.Cursor, habits: dict[int, Habit], repetition: Repetition
|
||||||
cursor.execute(
|
):
|
||||||
"""
|
"""Insert the repetition into a sqlite3 table suitable for Loop.
|
||||||
INSERT INTO
|
Parameters:
|
||||||
Repetitions(id, habit, timestamp, value)
|
:param c: SQL cursor of database to query.
|
||||||
VALUES (NULL, ?, ?, ?)
|
:sql_id: Internal sqlite database id of the habit the repetition belongs to.
|
||||||
""",
|
"""
|
||||||
(habit_id, timestamp, value),
|
for sql_id, habit in habits.items():
|
||||||
)
|
if repetition.habit_uuid == habit.uuid:
|
||||||
except sqlite3.IntegrityError:
|
try:
|
||||||
# TODO better error handling
|
cursor.execute(
|
||||||
print(f"fail to register {habit_id}: timestamp {timestamp} not unique")
|
"""
|
||||||
|
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."
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue