Spaces:
Sleeping
Sleeping
Upload 11 files
Browse files- pages/1_Dashboard.py +35 -0
- pages/2_Pomodoro_Timer.py +53 -0
- pages/3_Expenses_Tracker.py +45 -0
- pages/4_ToDo_List.py +37 -0
- pages/5_Medicine_Tracker.py +44 -0
- pages/6_Sleep_Tracker.py +36 -0
- requirements.txt +3 -0
- streamlit_app.py +32 -0
- utils/__init__.py +2 -0
- utils/charts.py +26 -0
- utils/data_utils.py +30 -0
pages/1_Dashboard.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
from datetime import date, datetime
|
| 4 |
+
from utils import load_data
|
| 5 |
+
from utils.charts import expenses_pie, sleep_line
|
| 6 |
+
|
| 7 |
+
st.title("📊 Daily Overview")
|
| 8 |
+
|
| 9 |
+
# --- Load data ---
|
| 10 |
+
expenses = load_data("expenses.json", [])
|
| 11 |
+
todos = load_data("todos.json", [])
|
| 12 |
+
medicines = load_data("medicines.json", [])
|
| 13 |
+
sleep_records = load_data("sleep.json", [])
|
| 14 |
+
pomodoros = load_data("pomodoro_history.json", [])
|
| 15 |
+
|
| 16 |
+
today = date.today().isoformat()
|
| 17 |
+
|
| 18 |
+
# --- Quick stats ---
|
| 19 |
+
daily_expenses = sum(e['amount'] for e in expenses if e['date'] == today)
|
| 20 |
+
completed_tasks = sum(1 for t in todos if t['done'])
|
| 21 |
+
pending_tasks = len(todos) - completed_tasks
|
| 22 |
+
upcoming_meds = [m for m in medicines if m['date'] == today and not m.get('taken')]
|
| 23 |
+
|
| 24 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 25 |
+
col1.metric("Today's Spend", f"₹{daily_expenses:,.2f}")
|
| 26 |
+
col2.metric("Tasks Pending", pending_tasks)
|
| 27 |
+
col3.metric("Pomodoro Sessions", len(pomodoros))
|
| 28 |
+
col4.metric("Meds Remaining", len(upcoming_meds))
|
| 29 |
+
|
| 30 |
+
# --- Compact charts ---
|
| 31 |
+
st.subheader("Expenses Breakdown")
|
| 32 |
+
st.plotly_chart(expenses_pie(expenses), use_container_width=True)
|
| 33 |
+
|
| 34 |
+
st.subheader("Sleep Trend")
|
| 35 |
+
st.plotly_chart(sleep_line(sleep_records), use_container_width=True)
|
pages/2_Pomodoro_Timer.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from datetime import datetime, timedelta
|
| 3 |
+
from utils import save_data
|
| 4 |
+
|
| 5 |
+
st.title("⏱️ Pomodoro Timer")
|
| 6 |
+
|
| 7 |
+
# --- User settings ---
|
| 8 |
+
work_minutes = st.number_input("Work duration (minutes)", 10, 120, 25)
|
| 9 |
+
break_minutes = st.number_input("Break duration (minutes)", 1, 60, 5)
|
| 10 |
+
|
| 11 |
+
# --- Session state initialisation ---
|
| 12 |
+
if 'pomodoro_running' not in st.session_state:
|
| 13 |
+
st.session_state.pomodoro_running = False
|
| 14 |
+
st.session_state.pomodoro_start = None
|
| 15 |
+
st.session_state.pomodoro_mode = 'Work' # or 'Break'
|
| 16 |
+
|
| 17 |
+
def start_timer():
|
| 18 |
+
st.session_state.pomodoro_running = True
|
| 19 |
+
st.session_state.pomodoro_start = datetime.now()
|
| 20 |
+
st.session_state.pomodoro_mode = 'Work'
|
| 21 |
+
|
| 22 |
+
def stop_timer():
|
| 23 |
+
st.session_state.pomodoro_running = False
|
| 24 |
+
st.session_state.pomodoro_start = None
|
| 25 |
+
|
| 26 |
+
# --- Buttons ---
|
| 27 |
+
col1, col2 = st.columns(2)
|
| 28 |
+
if col1.button("Start" if not st.session_state.pomodoro_running else "Restart"):
|
| 29 |
+
start_timer()
|
| 30 |
+
if col2.button("Stop"):
|
| 31 |
+
stop_timer()
|
| 32 |
+
|
| 33 |
+
# --- Timer display ---
|
| 34 |
+
if st.session_state.pomodoro_running and st.session_state.pomodoro_start:
|
| 35 |
+
elapsed = (datetime.now() - st.session_state.pomodoro_start).total_seconds()
|
| 36 |
+
target = work_minutes * 60 if st.session_state.pomodoro_mode == 'Work' else break_minutes * 60
|
| 37 |
+
remaining = max(0, target - elapsed)
|
| 38 |
+
mins, secs = divmod(int(remaining), 60)
|
| 39 |
+
st.header(f"{st.session_state.pomodoro_mode} — {mins:02d}:{secs:02d}")
|
| 40 |
+
if remaining == 0:
|
| 41 |
+
# switch mode
|
| 42 |
+
st.session_state.pomodoro_mode = 'Break' if st.session_state.pomodoro_mode == 'Work' else 'Work'
|
| 43 |
+
st.session_state.pomodoro_start = datetime.now()
|
| 44 |
+
|
| 45 |
+
# --- History table ---
|
| 46 |
+
if 'pomodoro_history' not in st.session_state:
|
| 47 |
+
st.session_state.pomodoro_history = []
|
| 48 |
+
|
| 49 |
+
st.subheader("Session History")
|
| 50 |
+
st.table(st.session_state.pomodoro_history)
|
| 51 |
+
|
| 52 |
+
# --- Persist history ---
|
| 53 |
+
save_data(st.session_state.pomodoro_history, "pomodoro_history.json")
|
pages/3_Expenses_Tracker.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
from utils import load_data, save_data
|
| 4 |
+
from utils.charts import expenses_pie
|
| 5 |
+
from datetime import date
|
| 6 |
+
|
| 7 |
+
st.title("💰 Expenses Tracker")
|
| 8 |
+
|
| 9 |
+
if 'expenses' not in st.session_state:
|
| 10 |
+
st.session_state.expenses = load_data("expenses.json", [])
|
| 11 |
+
|
| 12 |
+
# --- Add / edit form ---
|
| 13 |
+
st.subheader("Add / Edit Expense")
|
| 14 |
+
with st.form("expense_form", clear_on_submit=True):
|
| 15 |
+
amount = st.number_input("Amount", min_value=0.0, step=0.5)
|
| 16 |
+
category_list = list({e['category'] for e in st.session_state.expenses})
|
| 17 |
+
new_category = st.text_input("New category (leave blank to use existing)")
|
| 18 |
+
category = st.selectbox("Category", options=category_list) if not new_category else new_category
|
| 19 |
+
date_input = st.date_input("Date", value=date.today())
|
| 20 |
+
description = st.text_input("Description")
|
| 21 |
+
status = st.selectbox("Status", ["Paid", "Unpaid"])
|
| 22 |
+
submitted = st.form_submit_button("Save")
|
| 23 |
+
|
| 24 |
+
if submitted:
|
| 25 |
+
exp = {
|
| 26 |
+
"amount": amount,
|
| 27 |
+
"category": category,
|
| 28 |
+
"date": date_input.isoformat(),
|
| 29 |
+
"description": description,
|
| 30 |
+
"status": status
|
| 31 |
+
}
|
| 32 |
+
st.session_state.expenses.append(exp)
|
| 33 |
+
save_data(st.session_state.expenses, "expenses.json")
|
| 34 |
+
st.success("Expense saved!")
|
| 35 |
+
|
| 36 |
+
# --- Display table ---
|
| 37 |
+
st.subheader("All Expenses")
|
| 38 |
+
df = pd.DataFrame(st.session_state.expenses)
|
| 39 |
+
if not df.empty:
|
| 40 |
+
st.dataframe(df)
|
| 41 |
+
csv = df.to_csv(index=False).encode('utf-8')
|
| 42 |
+
st.download_button("📥 Download CSV", csv, "expenses.csv", mime="text/csv")
|
| 43 |
+
st.plotly_chart(expenses_pie(st.session_state.expenses), use_container_width=True)
|
| 44 |
+
else:
|
| 45 |
+
st.info("No expenses recorded yet.")
|
pages/4_ToDo_List.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from utils import load_data, save_data
|
| 3 |
+
|
| 4 |
+
st.title("✅ To‑Do List")
|
| 5 |
+
|
| 6 |
+
if 'todos' not in st.session_state:
|
| 7 |
+
st.session_state.todos = load_data("todos.json", [])
|
| 8 |
+
|
| 9 |
+
# --- Add task ---
|
| 10 |
+
new_task = st.text_input("Add a new task")
|
| 11 |
+
if st.button("Add") and new_task.strip():
|
| 12 |
+
st.session_state.todos.append({"task": new_task, "done": False})
|
| 13 |
+
save_data(st.session_state.todos, "todos.json")
|
| 14 |
+
|
| 15 |
+
# --- Filter ---
|
| 16 |
+
filter_opt = st.radio("Filter", ["All", "Active", "Completed"], horizontal=True)
|
| 17 |
+
|
| 18 |
+
# --- Task list ---
|
| 19 |
+
def task_filter(item):
|
| 20 |
+
if filter_opt == "Active":
|
| 21 |
+
return not item['done']
|
| 22 |
+
if filter_opt == "Completed":
|
| 23 |
+
return item['done']
|
| 24 |
+
return True
|
| 25 |
+
|
| 26 |
+
for i, todo in enumerate(st.session_state.todos):
|
| 27 |
+
if task_filter(todo):
|
| 28 |
+
cols = st.columns([0.1, 0.8, 0.1])
|
| 29 |
+
done = cols[0].checkbox("", value=todo['done'], key=f"todo_{i}")
|
| 30 |
+
if done != todo['done']:
|
| 31 |
+
todo['done'] = done
|
| 32 |
+
save_data(st.session_state.todos, "todos.json")
|
| 33 |
+
cols[1].write(todo['task'])
|
| 34 |
+
if cols[2].button("🗑️", key=f"del_{i}"):
|
| 35 |
+
st.session_state.todos.pop(i)
|
| 36 |
+
save_data(st.session_state.todos, "todos.json")
|
| 37 |
+
st.experimental_rerun()
|
pages/5_Medicine_Tracker.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from datetime import date, datetime
|
| 3 |
+
from utils import load_data, save_data
|
| 4 |
+
|
| 5 |
+
st.title("💊 Medicine Tracker")
|
| 6 |
+
|
| 7 |
+
if 'medicines' not in st.session_state:
|
| 8 |
+
st.session_state.medicines = load_data("medicines.json", [])
|
| 9 |
+
|
| 10 |
+
with st.form("add_med"):
|
| 11 |
+
name = st.text_input("Medicine Name")
|
| 12 |
+
dosage = st.text_input("Dosage (e.g., 500mg)")
|
| 13 |
+
time_of_day = st.selectbox("Time", ["Morning", "Afternoon", "Evening", "Night"])
|
| 14 |
+
meal = st.selectbox("Meal", ["Before Meal", "After Meal"])
|
| 15 |
+
date_input = st.date_input("Date", date.today())
|
| 16 |
+
submitted = st.form_submit_button("Add")
|
| 17 |
+
|
| 18 |
+
if submitted and name:
|
| 19 |
+
st.session_state.medicines.append({
|
| 20 |
+
"name": name,
|
| 21 |
+
"dosage": dosage,
|
| 22 |
+
"time": time_of_day,
|
| 23 |
+
"meal": meal,
|
| 24 |
+
"date": date_input.isoformat(),
|
| 25 |
+
"taken": False
|
| 26 |
+
})
|
| 27 |
+
save_data(st.session_state.medicines, "medicines.json")
|
| 28 |
+
st.success("Medicine logged.")
|
| 29 |
+
|
| 30 |
+
# --- Upcoming reminders ---
|
| 31 |
+
today = date.today().isoformat()
|
| 32 |
+
upcoming = [m for m in st.session_state.medicines if m['date'] == today and not m['taken']]
|
| 33 |
+
st.subheader("Today's Pending Doses")
|
| 34 |
+
for med in upcoming:
|
| 35 |
+
st.info(f"{med['time']} — {med['name']} {med['dosage']} ({med['meal']})")
|
| 36 |
+
if st.button("Mark Taken", key=f"take_{med['name']}_{med['time']}"):
|
| 37 |
+
med['taken'] = True
|
| 38 |
+
save_data(st.session_state.medicines, "medicines.json")
|
| 39 |
+
st.experimental_rerun()
|
| 40 |
+
|
| 41 |
+
# --- History ---
|
| 42 |
+
st.subheader("All Records")
|
| 43 |
+
if st.session_state.medicines:
|
| 44 |
+
st.dataframe(st.session_state.medicines)
|
pages/6_Sleep_Tracker.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from datetime import date, datetime, timedelta
|
| 3 |
+
from utils import load_data, save_data
|
| 4 |
+
from utils.charts import sleep_line
|
| 5 |
+
|
| 6 |
+
st.title("🛌 Sleep Tracker")
|
| 7 |
+
|
| 8 |
+
if 'sleep' not in st.session_state:
|
| 9 |
+
st.session_state.sleep = load_data("sleep.json", [])
|
| 10 |
+
|
| 11 |
+
# --- Record sleep ---
|
| 12 |
+
st.subheader("Record Sleep")
|
| 13 |
+
with st.form("sleep_form", clear_on_submit=True):
|
| 14 |
+
sleep_date = st.date_input("Date", date.today())
|
| 15 |
+
bed_time = st.time_input("Bed Time")
|
| 16 |
+
wake_time = st.time_input("Wake Time")
|
| 17 |
+
submitted = st.form_submit_button("Save")
|
| 18 |
+
|
| 19 |
+
if submitted:
|
| 20 |
+
bed_dt = datetime.combine(sleep_date, bed_time)
|
| 21 |
+
wake_dt = datetime.combine(sleep_date + timedelta(days=1 if wake_time < bed_time else 0), wake_time)
|
| 22 |
+
hours = round((wake_dt - bed_dt).total_seconds() / 3600, 2)
|
| 23 |
+
st.session_state.sleep.append({
|
| 24 |
+
"date": sleep_date.isoformat(),
|
| 25 |
+
"hours": hours
|
| 26 |
+
})
|
| 27 |
+
save_data(st.session_state.sleep, "sleep.json")
|
| 28 |
+
st.success(f"Recorded {hours} hrs of sleep.")
|
| 29 |
+
|
| 30 |
+
# --- Display ---
|
| 31 |
+
st.subheader("Last 7 Entries")
|
| 32 |
+
if st.session_state.sleep:
|
| 33 |
+
st.dataframe(st.session_state.sleep[-7:])
|
| 34 |
+
st.plotly_chart(sleep_line(st.session_state.sleep), use_container_width=True)
|
| 35 |
+
else:
|
| 36 |
+
st.info("No sleep data yet.")
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit>=1.33
|
| 2 |
+
plotly>=5.19
|
| 3 |
+
pandas>=2.2
|
streamlit_app.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from utils.data_utils import ensure_data_file, load_data, save_data
|
| 3 |
+
|
| 4 |
+
# -------- Page Config -------- #
|
| 5 |
+
st.set_page_config(
|
| 6 |
+
page_title="Life Dashboard",
|
| 7 |
+
page_icon="🌟",
|
| 8 |
+
layout="wide"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
# -------- Initialise Global Session State -------- #
|
| 12 |
+
default_lists = {
|
| 13 |
+
'expenses': [],
|
| 14 |
+
'todos': [],
|
| 15 |
+
'medicines': [],
|
| 16 |
+
'sleep': [],
|
| 17 |
+
'pomodoro_history': []
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
for key, val in default_lists.items():
|
| 21 |
+
if key not in st.session_state:
|
| 22 |
+
st.session_state[key] = load_data(f"{key}.json", val)
|
| 23 |
+
|
| 24 |
+
# -------- Simple landing content -------- #
|
| 25 |
+
st.title("Welcome to Your Life Dashboard ✨")
|
| 26 |
+
st.markdown(
|
| 27 |
+
"This home page just sets things up. Use the sidebar to open the individual tracker pages. "
|
| 28 |
+
"All data you enter is saved locally in **./data/**. Enjoy!")
|
| 29 |
+
|
| 30 |
+
# -------- Persist session data on every run -------- #
|
| 31 |
+
for key in default_lists:
|
| 32 |
+
save_data(st.session_state[key], f"{key}.json")
|
utils/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Re-export utility helpers."""
|
| 2 |
+
from .data_utils import load_data, save_data, ensure_data_file
|
utils/charts.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Chart helper functions."""
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import plotly.express as px
|
| 4 |
+
from typing import List, Dict
|
| 5 |
+
|
| 6 |
+
def expenses_pie(expenses: List[Dict]) -> 'plotly.graph_objs._figure.Figure':
|
| 7 |
+
"""Return a pie chart showing paid/unpaid by category."""
|
| 8 |
+
if not expenses:
|
| 9 |
+
return px.pie(title="No expense data yet")
|
| 10 |
+
df = pd.DataFrame(expenses)
|
| 11 |
+
df['status_label'] = df['category'] + ' - ' + df['status']
|
| 12 |
+
return px.pie(
|
| 13 |
+
df,
|
| 14 |
+
names='status_label',
|
| 15 |
+
values='amount',
|
| 16 |
+
title='Expenses: Paid vs Unpaid by Category'
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
def sleep_line(sleep_records: List[Dict]) -> 'plotly.graph_objs._figure.Figure':
|
| 20 |
+
"""Return a 7‑day line graph of sleep hours."""
|
| 21 |
+
if not sleep_records:
|
| 22 |
+
return px.line(title="No sleep data yet")
|
| 23 |
+
df = pd.DataFrame(sleep_records)
|
| 24 |
+
df['date'] = pd.to_datetime(df['date'])
|
| 25 |
+
df = df.sort_values('date').tail(7)
|
| 26 |
+
return px.line(df, x='date', y='hours', markers=True, title='Sleep Hours (Last 7 Days)')
|
utils/data_utils.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utility helpers for data persistence and shared logic."""
|
| 2 |
+
import json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
|
| 7 |
+
DATA_DIR.mkdir(exist_ok=True)
|
| 8 |
+
|
| 9 |
+
def _file_path(filename: str) -> Path:
|
| 10 |
+
"""Return path to a data file inside DATA_DIR."""
|
| 11 |
+
return DATA_DIR / filename
|
| 12 |
+
|
| 13 |
+
def ensure_data_file(filename: str, default: Any) -> None:
|
| 14 |
+
"""Create a data file with *default* content if it does not exist."""
|
| 15 |
+
fp = _file_path(filename)
|
| 16 |
+
if not fp.exists():
|
| 17 |
+
save_data(default, filename)
|
| 18 |
+
|
| 19 |
+
def load_data(filename: str, default: Any):
|
| 20 |
+
"""Load JSON data; create with *default* if missing."""
|
| 21 |
+
ensure_data_file(filename, default)
|
| 22 |
+
with open(_file_path(filename), "r", encoding="utf-8") as f:
|
| 23 |
+
return json.load(f)
|
| 24 |
+
|
| 25 |
+
def save_data(data: Any, filename: str) -> None:
|
| 26 |
+
"""Write JSON data atomically."""
|
| 27 |
+
tmp = _file_path(filename).with_suffix(".tmp")
|
| 28 |
+
with open(tmp, "w", encoding="utf-8") as f:
|
| 29 |
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
| 30 |
+
tmp.replace(_file_path(filename))
|