Refactor data into dataclasses

This commit is contained in:
Marty Oehme 2021-12-02 23:08:58 +01:00
parent c5b532b618
commit 76bedd42ee
Signed by: Marty
GPG Key ID: B7538B8F50A1C800
7 changed files with 197 additions and 62 deletions

View File

@ -1,3 +1,6 @@
from habitmove.loopdata import Habit
def migrate(db, trackers): def migrate(db, trackers):
c = db.cursor() c = db.cursor()
habits = trackers_to_habits(trackers) habits = trackers_to_habits(trackers)
@ -10,40 +13,33 @@ def migrate(db, trackers):
return habits return habits
# By default set goal to half of max value
NOMIE_MAX_TO_TARGET_VALUE_RATIO = 2 NOMIE_MAX_TO_TARGET_VALUE_RATIO = 2
def trackers_to_habits(trackers): def trackers_to_habits(trackers):
habits = [] habits = []
for tracker_name in trackers.keys(): for t in trackers:
t = trackers[tracker_name]
habits.append( habits.append(
{ Habit(
"archived": 0 if t["hidden"] == False else 1, archived=t.hidden,
"color": 11 if t.get("score", 0) != "-1" else 0, color=11 if t.score != "-1" else 0,
"description": t["tag"], description=t.tag,
"freq_den": 1, name=f"{t.emoji} {t.label}",
"freq_num": 1, unit="" if t.uom == "num" else t.uom,
"highlight": 0, uuid=t.id,
"name": f"{t['emoji']} {t['label']}", )
"question": "",
"position": 0,
"unit": "" if t["uom"] == "num" else t["uom"],
"uuid": t["id"],
}
) )
if t["type"] == "range" and len(habits) > 0: if t.type == "range" and len(habits) > 0:
habits[-1]["type"] = 1 habits[-1].type = 1
# nomie only has concept of max value, # nomie only has concept of max value,
# use a percentage of it for Loop range target # use a percentage of it for Loop range target
habits[-1]["target_value"] = ( habits[-1].target_value = int(t.max) // NOMIE_MAX_TO_TARGET_VALUE_RATIO
int(t["max"]) // NOMIE_MAX_TO_TARGET_VALUE_RATIO
)
return habits return habits
def check_habit_duplicate(cursor, habit): def check_habit_duplicate(cursor, habit):
cursor.execute("select name from Habits where uuid = ?", [habit["uuid"]]) cursor.execute("select name from Habits where uuid = ?", [habit.uuid])
name = cursor.fetchone() name = cursor.fetchone()
if name: if name:
return name[0] return name[0]
@ -54,15 +50,13 @@ def add_to_database(cursor, habit):
"""Takes a habit in the form of a dictionary and inserts it into the Habits table. """Takes a habit in the form of a dictionary and inserts it into the Habits table.
Parameters: Parameters:
cursor (db.cursor): SQL executing cursor cursor (db.cursor): SQL executing cursor
habit (dict): A Loop habit to be added to the database. Must contain a minimum of habit (Habit): A Loop habit to be added to the database.
'archived','color','description','freq_den','freq_num','highlight','name',
'question','position','unit','uuid' as keys.
""" """
habit_data = habit.__dict__
placeholder = ", ".join("?" * len(habit)) placeholder = ", ".join("?" * len(habit_data))
columns = ", ".join(habit.keys()) columns = ", ".join(habit_data.keys())
sql = "insert into `{table}` ({columns}) values ({values});".format( sql = "insert into `{table}` ({columns}) values ({values});".format(
table="Habits", columns=columns, values=placeholder table="Habits", columns=columns, values=placeholder
) )
values = list(habit.values()) values = list(habit_data.values())
cursor.execute(sql, values) cursor.execute(sql, values)

25
habitmove/loopdata.py Normal file
View File

@ -0,0 +1,25 @@
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.
@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
# TODO test post init uuid setting
def __post_init__(self):
if not self.uuid:
self.uuid = uuid4().hex

View File

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
import json import json
import re
from habitmove.nomiedata import Tracker, Event, Activity, NomieImport
def load_file(filename): def load_file(filename):
@ -23,21 +25,77 @@ def confirmation_question(question, default_no=True):
# display stats and ask user to confirm if they seem okay # display stats and ask user to confirm if they seem okay
def verify_continue(data): def verify_continue(data: NomieImport):
trackers = "" trackers = ""
for t in data["trackers"]: for t in data.trackers:
trackers += t + ", " trackers += t.label + ", "
print(f"Exporting from nomie {data['nomie']['number']}:") trackers = trackers[:-2]
print(f"Exporting from nomie {data.version}:")
print(f"Found trackers: {trackers}") print(f"Found trackers: {trackers}")
print(f"Found events: {len(data['events'])} entries.") print(
f"Found events: {len(data.events)} entries, containing {len(data.activities)} 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.")
exit(0) exit(0)
def get_trackers(raw_trackers):
tracker_list = list[Tracker]()
for tracker_tuple in raw_trackers.items():
tracker_list.append(Tracker(**tracker_tuple[1]))
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 extract_tags_from_text(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
def get_activities(tracker_list, note_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]))
return activities
# return the data belonging to nomie # return the data belonging to nomie
def get_data(file, interactive): def get_data(file, interactive=True):
nomie_data = load_file(file) raw_data = load_file(file)
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)
data = NomieImport(nomie_version, tracker_list, note_list, activity_list)
if interactive: if interactive:
verify_continue(nomie_data) verify_continue(data)
return nomie_data
return data

58
habitmove/nomiedata.py Normal file
View File

@ -0,0 +1,58 @@
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
class Tracker:
color: str
emoji: str
hidden: bool
id: str
ignore_zeros: bool
label: str
math: str
one_tap: bool
tag: str
type: str
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
# 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: [])
# 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
class Activity:
tracker: Tracker
value: Optional[int] = 1
@dataclass
class NomieImport:
version: str
trackers: list[Tracker]
events: list[Event]
activities: list[Activity]

View File

@ -26,8 +26,10 @@ def get_all_repetitions(habits, events):
""" """
repetitions = [] repetitions = []
for event in events: for event in events:
for habit in habits: for habit_id in habits.keys():
reps = tags_to_repetitions(habit, extract_tags(event["note"]), event["end"]) reps = tags_to_repetitions(
habit_id, habits[habit_id], extract_tags(event.text), event.end
)
if reps: if reps:
repetitions.extend(reps) repetitions.extend(reps)
@ -51,7 +53,7 @@ def extract_tags(text, tagmarker="#"):
# does not do: # does not do:
# non-range habits but still #habit(3) number included, adding multiple? # non-range habits but still #habit(3) number included, adding multiple?
def tags_to_repetitions(habit, tags, timestamp): def tags_to_repetitions(habit_id, habit, tags, timestamp):
"""Return a list of all repetitions generated from the tags and habits passed in. """Return a list of all repetitions generated from the tags and habits passed in.
Parameters: Parameters:
habits (list): Collection of habits, with minimum necessary fields description and id. habits (list): Collection of habits, with minimum necessary fields description and id.
@ -64,10 +66,10 @@ def tags_to_repetitions(habit, tags, timestamp):
""" """
reps = [] reps = []
for tag in tags: for tag in tags:
if habit["description"] in tag[0]: if habit.description in tag[0]:
repetition = {"id": habit["id"], "timestamp": timestamp} repetition = {"id": habit_id, "timestamp": timestamp}
if tag[1]: if tag[1]:
if "type" in habit and habit["type"] == 1: if habit.type == 1:
repetition["value"] = tag[1] * LOOP_RANGE_VALUE_MULTIPLIER repetition["value"] = tag[1] * LOOP_RANGE_VALUE_MULTIPLIER
reps.append(repetition) reps.append(repetition)
return reps return reps
@ -78,15 +80,16 @@ 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.
Parameters: Parameters:
c (sqlite.db.cursor): SQL cursor of database to query. c (sqlite.db.cursor): SQL cursor of database to query.
habitlist (list): Full habit collection to return a simplified view of. habitlist (list[Habit]): Full habit collection to return a simplified view of.
Returns: Returns:
habitlist (list): The full habit collection passed in unchanged, habit_id_dict (dict[Habit]): The habit collection as a dict with the keys
except for an id added to each habit corresponding consisting of the habit's sqlite database ID.
to their id in sqlite database and Loop repetitions.
""" """
with_id = habitlist.copy() with_id = {}
for h in with_id: for h in habitlist:
h["id"] = fetch_habit_id(c, h["uuid"]) sql_id = fetch_habit_id(c, h.uuid)
with_id[sql_id] = h
return with_id return with_id

View File

@ -5,7 +5,7 @@ def create_database(name):
return sqlite3.connect(name) return sqlite3.connect(name)
# better way to do the above # TODO better way to do the above
# def create_connection(db_file): # def create_connection(db_file):
# """create a database connection to the SQLite database # """create a database connection to the SQLite database
# specified by db_file # specified by db_file

19
run.py
View File

@ -3,19 +3,20 @@ import habitmove.schema as schema
import habitmove.habits as habits import habitmove.habits as habits
import habitmove.repetitions as rep import habitmove.repetitions as rep
import habitmove.nomie as nomie import habitmove.nomie as nomie
from habitmove.nomiedata import NomieImport
import sys import sys
def migrate(trackers, events): def migrate(data: NomieImport):
db = schema.migrate("output.db") db = schema.migrate("output.db")
if trackers is not None: if data.trackers is not None:
habitlist = habits.migrate(db, trackers) habitlist = habits.migrate(db, data.trackers)
if events is not None: if data.events is not None:
rep.migrate(db, habitlist, events) rep.migrate(db, habitlist, data.events)
db.commit() db.commit()
db.close() db.close()
@ -23,13 +24,9 @@ def migrate(trackers, events):
def main(): def main():
file = sys.argv[1] file = sys.argv[1]
data = nomie.get_data(file, True) data = nomie.get_data(file)
migrate(data["trackers"], data["events"]) migrate(data)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
# things to have as (data) classes:
# tracker, event, tag?
# habit, repetition