-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdatabase.py
More file actions
248 lines (219 loc) · 10.4 KB
/
database.py
File metadata and controls
248 lines (219 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
"""
Designed and Created by Bahamut 0 - Ernest Hardison
"""
import sqlite3
from datetime import datetime
from typing import List, Optional, Any
class Database:
"""
Handles all interactions with the SQLite database.
Manages connections, migrations, and CRUD operations for habits and logs.
"""
def __init__(self, db_name="habits.db"):
"""Initialize the database manager with a specific file name."""
self.db_name = db_name
self.init_db()
def get_connection(self):
"""Creates and returns a thread-safe connection to the SQLite database."""
return sqlite3.connect(self.db_name)
def init_db(self):
"""
Initializes the database schema.
Creates 'habits' and 'logs' tables if they don't exist.
Performs simple migrations to add new columns (start_date, end_date) to existing tables.
"""
with self.get_connection() as conn:
cursor = conn.cursor()
# Habits table
cursor.execute("""
CREATE TABLE IF NOT EXISTS habits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at TEXT NOT NULL,
type TEXT DEFAULT 'simple', -- 'simple' or 'quantitative'
unit TEXT,
goal REAL DEFAULT 1,
start_date TEXT,
end_date TEXT,
archived BOOLEAN DEFAULT 0
)
""")
# Simple Migration for existing tables (Try/Except to add columns)
try:
cursor.execute("ALTER TABLE habits ADD COLUMN start_date TEXT")
except sqlite3.OperationalError:
pass # Already exists
try:
cursor.execute("ALTER TABLE habits ADD COLUMN end_date TEXT")
except sqlite3.OperationalError:
pass
# Logs table (for tracking progress)
cursor.execute("""
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
habit_id INTEGER NOT NULL,
date TEXT NOT NULL, -- YYYY-MM-DD
value REAL NOT NULL,
FOREIGN KEY (habit_id) REFERENCES habits (id)
)
""")
# Index for performance on date lookups (Crucial for 2-year queries)
cursor.execute("CREATE INDEX IF NOT EXISTS idx_logs_habit_date ON logs (habit_id, date)")
# User Profile table
cursor.execute("""
CREATE TABLE IF NOT EXISTS user_profile (
id INTEGER PRIMARY KEY CHECK (id = 1), -- Singleton row
name TEXT DEFAULT '',
birthdate TEXT DEFAULT '',
phone TEXT DEFAULT '',
email TEXT DEFAULT '',
goals TEXT DEFAULT ''
)
""")
conn.commit()
def get_profile(self) -> dict:
"""Retrieves the user profile data. Returns default dict if empty."""
with self.get_connection() as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT * FROM user_profile WHERE id = 1")
row = cursor.fetchone()
if row:
return dict(row)
return {"name": "", "birthdate": "", "phone": "", "email": "", "goals": ""}
def update_profile(self, name: str, birthdate: str, phone: str, email: str, goals: str):
"""Updates or inserts the singleton user profile."""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO user_profile (id, name, birthdate, phone, email, goals)
VALUES (1, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name=excluded.name,
birthdate=excluded.birthdate,
phone=excluded.phone,
email=excluded.email,
goals=excluded.goals
""", (name, birthdate, phone, email, goals))
def add_habit(self, name: str, type: str = 'simple', unit: str = '', goal: float = 1, start_date: str = None, end_date: str = None) -> int:
"""
Inserts a new habit into the database.
Args:
name: The name of the habit.
type: 'simple' (yes/no) or 'quantitative' (numeric).
unit: Unit of measurement for quantitative habits (e.g., 'ml', 'min').
goal: Daily target value.
start_date: Date to start tracking (YYYY-MM-DD). Defaults to today.
end_date: Optional end date (YYYY-MM-DD).
Returns:
int: The ID of the newly created habit.
"""
with self.get_connection() as conn:
cursor = conn.cursor()
created_at = datetime.now().isoformat()
if not start_date:
start_date = datetime.now().strftime("%Y-%m-%d")
cursor.execute(
"INSERT INTO habits (name, created_at, type, unit, goal, start_date, end_date) VALUES (?, ?, ?, ?, ?, ?, ?)",
(name, created_at, type, unit, goal, start_date, end_date)
)
return cursor.lastrowid
def get_all_habits(self) -> List[Any]:
"""
Retrieves all active (non-archived) habits, ordered by creation date (newest first).
Returns:
List[sqlite3.Row]: A list of row objects representing the habits.
"""
with self.get_connection() as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT * FROM habits WHERE archived = 0 ORDER BY created_at DESC")
return [dict(row) for row in cursor.fetchall()]
def log_habit(self, habit_id: int, date: str, value: float):
"""
Upsert logic: If a log exists for this date, update it. If not, insert it.
For simple habits (bool), value usually toggles, but here we enforce value.
"""
with self.get_connection() as conn:
cursor = conn.cursor()
# Check existing
cursor.execute("SELECT id, value FROM logs WHERE habit_id = ? AND date = ?", (habit_id, date))
existing = cursor.fetchone()
if existing:
# Update
new_value = existing[1] + value # Additive for quantitative
if value == 1 and existing[1] == 1: # Toggle logic for simple items handled in UI usually, but DB just stores
pass
# For this implementation, we will treat 'log' as 'set value' or 'add value'
# Let's assume the UI sends the DELTA or the FINAL value.
# To be safely robust, let's implement 'set_log' and 'add_log'.
# This function will be 'add_log' behavior for quantitative, 'set' for simple?
# Let's simplify: log_habit sets the value for specific logs, or increments.
# Actually, simpler: DELETE if 0, INSERT/UPDATE otherwise.
pass
def toggle_simple_habit(self, habit_id: int, date: str) -> bool:
"""Toggles a simple habit for a date. Returns new state (True/False)."""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT id FROM logs WHERE habit_id = ? AND date = ?", (habit_id, date))
existing = cursor.fetchone()
if existing:
cursor.execute("DELETE FROM logs WHERE id = ?", (existing[0],))
return False
else:
cursor.execute("INSERT INTO logs (habit_id, date, value) VALUES (?, ?, ?)", (habit_id, date, 1.0))
return True
def update_log_value(self, habit_id: int, date: str, value: float):
"""Sets a specific value for a date (for quantitative habits)."""
with self.get_connection() as conn:
cursor = conn.cursor()
if value <= 0:
cursor.execute("DELETE FROM logs WHERE habit_id = ? AND date = ?", (habit_id, date))
else:
cursor.execute("""
INSERT INTO logs (habit_id, date, value)
VALUES (?, ?, ?)
ON CONFLICT(id) DO UPDATE SET value = ? -- Wait, no unique constraint on (habit_id, date) yet?
""", (habit_id, date, value, value))
# SQLite upsert requires unique index/constraint.
# Let's fix the schema or handle logic manually.
# Manual is safer across sqlite versions if unsure.
cursor.execute("SELECT id FROM logs WHERE habit_id = ? AND date = ?", (habit_id, date))
row = cursor.fetchone()
if row:
cursor.execute("UPDATE logs SET value = ? WHERE id = ?", (value, row[0]))
else:
cursor.execute("INSERT INTO logs (habit_id, date, value) VALUES (?, ?, ?)", (habit_id, date, value))
def get_logs_for_habits(self, habit_ids: List[int], start_date: str, end_date: str) -> dict:
"""
Efficient batch fetch for dashboard.
Returns entries: { habit_id: { date: value } }
"""
if not habit_ids:
return {}
placeholders = ','.join('?' * len(habit_ids))
with self.get_connection() as conn:
cursor = conn.cursor()
query = f"""
SELECT habit_id, date, value
FROM logs
WHERE habit_id IN ({placeholders})
AND date BETWEEN ? AND ?
"""
params = habit_ids + [start_date, end_date]
cursor.execute(query, params)
result = {hid: {} for hid in habit_ids}
for row in cursor.fetchall():
h_id, date, val = row
result[h_id][date] = val
return result
def delete_habit(self, habit_id: int):
"""
Soft-deletes a habit by setting its 'archived' flag to 1.
Args:
habit_id: The ID of the habit to archive.
"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("UPDATE habits SET archived = 1 WHERE id = ?", (habit_id,))
db = Database()