First synculous 2 Big-Pickle pass.

This commit is contained in:
2026-02-12 23:07:48 -06:00
parent 25d05e0e86
commit 3e1134575b
26 changed files with 2729 additions and 59 deletions

224
core/routines.py Normal file
View File

@@ -0,0 +1,224 @@
"""
core/routines.py - Shared business logic for routines
This module contains reusable functions for routine operations
that can be called from API routes, bot commands, or scheduler.
"""
import uuid
from datetime import datetime, date
import core.postgres as postgres
def start_session(routine_id, user_uuid):
"""
Create and start a new routine session.
Returns the session object or None if routine not found.
"""
routine = postgres.select_one("routines", {"id": routine_id, "user_uuid": user_uuid})
if not routine:
return None
active_session = postgres.select_one(
"routine_sessions",
{"user_uuid": user_uuid, "status": "active"}
)
if active_session:
return {"error": "already_active", "session_id": active_session["id"]}
steps = postgres.select(
"routine_steps",
{"routine_id": routine_id},
order_by="position"
)
if not steps:
return {"error": "no_steps"}
session = {
"id": str(uuid.uuid4()),
"routine_id": routine_id,
"user_uuid": user_uuid,
"status": "active",
"current_step_index": 0,
}
result = postgres.insert("routine_sessions", session)
return {"session": result, "current_step": steps[0]}
def pause_session(session_id, user_uuid):
"""Pause an active session."""
session = postgres.select_one(
"routine_sessions",
{"id": session_id, "user_uuid": user_uuid}
)
if not session:
return None
if session.get("status") != "active":
return {"error": "not_active"}
result = postgres.update(
"routine_sessions",
{"status": "paused", "paused_at": datetime.now().isoformat()},
{"id": session_id}
)
return result
def resume_session(session_id, user_uuid):
"""Resume a paused session."""
session = postgres.select_one(
"routine_sessions",
{"id": session_id, "user_uuid": user_uuid}
)
if not session:
return None
if session.get("status") != "paused":
return {"error": "not_paused"}
result = postgres.update(
"routine_sessions",
{"status": "active", "paused_at": None},
{"id": session_id}
)
return result
def abort_session(session_id, user_uuid, reason=None):
"""Abort a session with optional reason."""
session = postgres.select_one(
"routine_sessions",
{"id": session_id, "user_uuid": user_uuid}
)
if not session:
return None
result = postgres.update(
"routine_sessions",
{
"status": "aborted",
"abort_reason": reason or "Aborted by user",
"completed_at": datetime.now().isoformat()
},
{"id": session_id}
)
return result
def complete_session(session_id, user_uuid):
"""Mark a session as completed and update streak."""
session = postgres.select_one(
"routine_sessions",
{"id": session_id, "user_uuid": user_uuid}
)
if not session:
return None
completed_at = datetime.now()
result = postgres.update(
"routine_sessions",
{"status": "completed", "completed_at": completed_at.isoformat()},
{"id": session_id}
)
_update_streak(user_uuid, session["routine_id"])
return result
def clone_template(template_id, user_uuid):
"""Clone a template to user's routines."""
template = postgres.select_one("routine_templates", {"id": template_id})
if not template:
return None
template_steps = postgres.select(
"routine_template_steps",
{"template_id": template_id},
order_by="position"
)
new_routine = {
"id": str(uuid.uuid4()),
"user_uuid": user_uuid,
"name": template["name"],
"description": template.get("description"),
"icon": template.get("icon"),
}
routine = postgres.insert("routines", new_routine)
for step in template_steps:
new_step = {
"id": str(uuid.uuid4()),
"routine_id": routine["id"],
"name": step["name"],
"instructions": step.get("instructions"),
"step_type": step.get("step_type", "generic"),
"duration_minutes": step.get("duration_minutes"),
"media_url": step.get("media_url"),
"position": step["position"],
}
postgres.insert("routine_steps", new_step)
return routine
def _update_streak(user_uuid, routine_id):
"""Update streak after completing a session. Resets if day was missed."""
today = date.today()
streak = postgres.select_one(
"routine_streaks",
{"user_uuid": user_uuid, "routine_id": routine_id}
)
if not streak:
new_streak = {
"id": str(uuid.uuid4()),
"user_uuid": user_uuid,
"routine_id": routine_id,
"current_streak": 1,
"longest_streak": 1,
"last_completed_date": today.isoformat(),
}
return postgres.insert("routine_streaks", new_streak)
last_completed = streak.get("last_completed_date")
if last_completed:
if isinstance(last_completed, str):
last_completed = date.fromisoformat(last_completed)
days_diff = (today - last_completed).days
if days_diff == 0:
return streak
elif days_diff == 1:
new_streak = streak["current_streak"] + 1
else:
new_streak = 1
else:
new_streak = 1
longest = max(streak["longest_streak"], new_streak)
postgres.update(
"routine_streaks",
{
"current_streak": new_streak,
"longest_streak": longest,
"last_completed_date": today.isoformat(),
},
{"id": streak["id"]}
)
return streak
def calculate_streak(user_uuid, routine_id):
"""Get current streak for a routine."""
streak = postgres.select_one(
"routine_streaks",
{"user_uuid": user_uuid, "routine_id": routine_id}
)
return streak
def get_active_session(user_uuid):
"""Get user's currently active session."""
return postgres.select_one(
"routine_sessions",
{"user_uuid": user_uuid, "status": "active"}
)

160
core/stats.py Normal file
View File

@@ -0,0 +1,160 @@
"""
core/stats.py - Statistics calculations for routines
This module contains functions for calculating routine statistics,
completion rates, streaks, and weekly summaries.
"""
from datetime import datetime, timedelta, date
import core.postgres as postgres
def get_routine_stats(routine_id, user_uuid, days=30):
"""
Get completion statistics for a routine over a period.
Returns dict with completion_rate, avg_duration, total_time, etc.
"""
sessions = postgres.select(
"routine_sessions",
{"routine_id": routine_id, "user_uuid": user_uuid},
limit=days * 3,
)
completed = sum(1 for s in sessions if s.get("status") == "completed")
aborted = sum(1 for s in sessions if s.get("status") == "aborted")
total_duration = sum(
s.get("actual_duration_minutes", 0) or 0
for s in sessions
if s.get("actual_duration_minutes")
)
avg_duration = total_duration / completed if completed > 0 else 0
completion_rate = (completed / len(sessions) * 100) if sessions else 0
return {
"total_sessions": len(sessions),
"completed": completed,
"aborted": aborted,
"completion_rate_percent": round(completion_rate, 1),
"avg_duration_minutes": round(avg_duration, 1),
"total_time_minutes": total_duration,
}
def get_user_streaks(user_uuid):
"""
Get all streaks for a user across all routines.
Returns list of streak objects with routine names.
"""
streaks = postgres.select("routine_streaks", {"user_uuid": user_uuid})
routines = postgres.select("routines", {"user_uuid": user_uuid})
routine_map = {r["id"]: r["name"] for r in routines}
result = []
for streak in streaks:
result.append({
"routine_id": streak["routine_id"],
"routine_name": routine_map.get(streak["routine_id"], "Unknown"),
"current_streak": streak["current_streak"],
"longest_streak": streak["longest_streak"],
"last_completed_date": streak.get("last_completed_date"),
})
return result
def get_weekly_summary(user_uuid):
"""
Get weekly progress summary for a user.
Returns total completed, total time, routines started, per-routine breakdown.
"""
routines = postgres.select("routines", {"user_uuid": user_uuid})
if not routines:
return {
"total_completed": 0,
"total_time_minutes": 0,
"routines_started": 0,
"routines": [],
}
week_ago = datetime.now() - timedelta(days=7)
sessions = postgres.select("routine_sessions", {"user_uuid": user_uuid})
week_sessions = [
s for s in sessions
if s.get("created_at") and s["created_at"] >= week_ago
]
completed = [s for s in week_sessions if s.get("status") == "completed"]
total_time = sum(
s.get("actual_duration_minutes", 0) or 0
for s in completed
if s.get("actual_duration_minutes")
)
routine_summaries = []
for routine in routines:
r_sessions = [s for s in week_sessions if s.get("routine_id") == routine["id"]]
r_completed = sum(1 for s in r_sessions if s.get("status") == "completed")
routine_summaries.append({
"routine_id": routine["id"],
"name": routine["name"],
"completed_this_week": r_completed,
})
return {
"total_completed": len(completed),
"total_time_minutes": total_time,
"routines_started": len(set(s.get("routine_id") for s in week_sessions)),
"routines": routine_summaries,
}
def calculate_completion_rate(sessions, completed_only=True):
"""Calculate completion rate from a list of sessions."""
if not sessions:
return 0.0
if completed_only:
completed = sum(1 for s in sessions if s.get("status") == "completed")
return (completed / len(sessions)) * 100
return 0.0
def get_monthly_summary(user_uuid, year=None, month=None):
"""
Get monthly progress summary.
Defaults to current month if year/month not specified.
"""
if year is None or month is None:
now = datetime.now()
year = now.year
month = now.month
start_date = datetime(year, month, 1)
if month == 12:
end_date = datetime(year + 1, 1, 1)
else:
end_date = datetime(year, month + 1, 1)
sessions = postgres.select("routine_sessions", {"user_uuid": user_uuid})
month_sessions = [
s for s in sessions
if s.get("created_at") and start_date <= s["created_at"] < end_date
]
completed = [s for s in month_sessions if s.get("status") == "completed"]
total_time = sum(
s.get("actual_duration_minutes", 0) or 0
for s in completed
if s.get("actual_duration_minutes")
)
return {
"year": year,
"month": month,
"total_sessions": len(month_sessions),
"completed": len(completed),
"total_time_minutes": total_time,
}