Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions src/blacki/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,11 @@ def _build_workout_tools() -> list[Any]:
from blacki.workouts import (
advance_training_cycle,
delete_workout,
get_exercise_progress,
get_last_workout,
get_todays_training,
get_todays_workout,
get_training_history,
get_training_metrics,
list_recent_workouts,
log_training,
log_workout,
set_training_program,
Expand All @@ -141,8 +139,6 @@ def _build_workout_tools() -> list[Any]:
update_training_metrics,
log_workout,
get_last_workout,
get_exercise_progress,
list_recent_workouts,
delete_workout,
set_workout_split,
get_todays_workout,
Expand Down
4 changes: 0 additions & 4 deletions src/blacki/workouts/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
from .tools import (
advance_training_cycle,
delete_workout,
get_exercise_progress,
get_last_workout,
get_todays_training,
get_todays_workout,
get_training_history,
get_training_metrics,
list_recent_workouts,
log_training,
log_workout,
set_training_program,
Expand All @@ -18,13 +16,11 @@
__all__ = [
"advance_training_cycle",
"delete_workout",
"get_exercise_progress",
"get_last_workout",
"get_todays_training",
"get_todays_workout",
"get_training_history",
"get_training_metrics",
"list_recent_workouts",
"log_workout",
"log_training",
"set_training_program",
Expand Down
110 changes: 0 additions & 110 deletions src/blacki/workouts/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,29 +71,6 @@ class WorkoutSession(BaseModel):
metrics: dict[str, Any] = Field(default_factory=dict)


class WorkoutSessionSummary(BaseModel):
"""Lightweight view for listing — no exercise data."""

id: int
workout_date: str
split_name: str
exercise_count: int
cycle_day: int | None = None
session_type: str | None = None
completion_status: str = "completed"


class ExerciseHistoryEntry(BaseModel):
"""One instance of an exercise across time for progressive overload tracking."""

workout_date: str
split_name: str
sets: list[SetDetail]
best_set_weight_kg: float
best_set_reps: int
total_volume_kg: float


class TrainingProgramDay(BaseModel):
"""One scheduled day in a rotating training program."""

Expand Down Expand Up @@ -470,43 +447,6 @@ async def get_latest_split_session(

return await self.get_session(row["id"], user_id)

async def get_recent_sessions(
self, user_id: str, limit: int = 10
) -> list[WorkoutSessionSummary]:
"""Returns lightweight view of recent sessions."""
limit = min(limit, 20)
rows = await self._fetch_all(
"""
SELECT
s.id,
s.workout_date,
s.split_name,
s.cycle_day,
s.session_type,
s.completion_status,
COUNT(e.id) as exercise_count
FROM workout_sessions s
LEFT JOIN workout_exercises e ON s.id = e.session_id
WHERE s.user_id = ?
GROUP BY s.id
ORDER BY s.workout_date DESC, s.created_at DESC
LIMIT ?
""",
(user_id, limit),
)
return [
WorkoutSessionSummary(
id=r["id"],
workout_date=r["workout_date"],
split_name=r["split_name"],
exercise_count=r["exercise_count"],
cycle_day=r["cycle_day"],
session_type=r["session_type"],
completion_status=r["completion_status"],
)
for r in rows
]

async def create_training_program(
self,
program: TrainingProgram,
Expand Down Expand Up @@ -811,56 +751,6 @@ async def get_latest_training_metrics(
)
return [self._row_to_training_metric(row) for row in rows]

async def get_exercise_history(
self, user_id: str, exercise_name: str, limit: int = 8
) -> list[ExerciseHistoryEntry]:
"""Returns the last N instances of a specific exercise."""
limit = min(limit, 8)
rows = await self._fetch_all(
"""
SELECT s.workout_date, s.split_name, e.sets
FROM workout_exercises e
JOIN workout_sessions s ON e.session_id = s.id
WHERE s.user_id = ? AND e.exercise_name = ?
ORDER BY s.workout_date DESC, s.created_at DESC
LIMIT ?
""",
(user_id, exercise_name.lower(), limit),
)

history = []
for r in rows:
sets_data = (
json.loads(r["sets"]) if isinstance(r["sets"], str) else r["sets"]
)
sets = [SetDetail(**s) for s in sets_data]

best_weight = 0.0
best_reps = 0
volume = 0.0

for s in sets:
if not s.is_warmup:
volume += s.weight_kg * s.reps
if s.weight_kg > best_weight or (
s.weight_kg == best_weight and s.reps > best_reps
):
best_weight = s.weight_kg
best_reps = s.reps

history.append(
ExerciseHistoryEntry(
workout_date=r["workout_date"],
split_name=r["split_name"],
sets=sets,
best_set_weight_kg=best_weight,
best_set_reps=best_reps,
total_volume_kg=volume,
)
)

return history

async def delete_session(self, session_id: int, user_id: str) -> bool:
"""Cascades to exercises."""
async with self._lock:
Expand Down
41 changes: 0 additions & 41 deletions src/blacki/workouts/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,47 +626,6 @@ async def get_last_workout(
}


async def get_exercise_progress(
tool_context: ToolContext,
exercise_name: str,
weeks: int = 4,
) -> dict[str, Any]:
"""Progressive overload view for a single exercise over N weeks."""
user_id = tool_context.user_id
if not user_id:
return {"status": "error", "message": "Missing user_id in tool_context"}

storage = get_storage()
# cap at 8 entries as per instructions
limit = min(weeks * 2, 8)

history = await storage.get_exercise_history(user_id, exercise_name, limit=limit)

return {
"status": "success",
"exercise_name": exercise_name.lower(),
"history": [h.model_dump() for h in history],
}


async def list_recent_workouts(
tool_context: ToolContext,
limit: int = 10,
) -> dict[str, Any]:
"""Overview of recent sessions (lightweight)."""
user_id = tool_context.user_id
if not user_id:
return {"status": "error", "message": "Missing user_id in tool_context"}

storage = get_storage()
sessions = await storage.get_recent_sessions(user_id, limit=limit)

return {
"status": "success",
"sessions": [s.model_dump() for s in sessions],
}


async def delete_workout(
tool_context: ToolContext,
session_id: int,
Expand Down
2 changes: 1 addition & 1 deletion tests/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ def test_returns_tools_when_available(self) -> None:

tools = _build_workout_tools()

assert len(tools) == 14
assert len(tools) == 12


class TestBuildSandboxTools:
Expand Down
108 changes: 2 additions & 106 deletions tests/workouts/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,110 +282,6 @@ async def test_get_session_wrong_user(self, storage) -> None:

assert result is None

@pytest.mark.asyncio
async def test_get_recent_sessions(self, storage) -> None:
"""Should get recent sessions with exercise counts."""
for i in range(3):
session = WorkoutSession(
user_id="user1",
workout_date=f"2026-04-{26 - i:02d}",
split_name="push" if i % 2 == 0 else "pull",
created_at=f"2026-04-{26 - i:02d}T10:00:00",
exercises=[
WorkoutExercise(
exercise_name=f"exercise {j}",
sets=[SetDetail(set_num=1, weight_kg=100, reps=10)],
)
for j in range(i + 1)
],
)
await storage.create_session(session)

sessions = await storage.get_recent_sessions("user1")

assert len(sessions) == 3
assert sessions[0].workout_date == "2026-04-26"
assert sessions[0].exercise_count == 1
assert sessions[1].exercise_count == 2
assert sessions[2].exercise_count == 3

@pytest.mark.asyncio
async def test_get_recent_sessions_respects_limit(self, storage) -> None:
"""Should limit the number of sessions returned."""
for i in range(15):
session = WorkoutSession(
user_id="user1",
workout_date=f"2026-04-{26 - i:02d}",
split_name="push",
created_at=f"2026-04-{26 - i:02d}T10:00:00",
exercises=[],
)
await storage.create_session(session)

sessions = await storage.get_recent_sessions("user1", limit=5)

assert len(sessions) == 5

@pytest.mark.asyncio
async def test_get_exercise_history(self, storage) -> None:
"""Should get exercise history with best sets."""
for i in range(3):
session = WorkoutSession(
user_id="user1",
workout_date=f"2026-04-{26 - i:02d}",
split_name="push",
created_at=f"2026-04-{26 - i:02d}T10:00:00",
exercises=[
WorkoutExercise(
exercise_name="bench press",
sets=[
SetDetail(
set_num=1,
weight_kg=100.0 + (2 - i) * 5,
reps=10 - (2 - i),
),
SetDetail(
set_num=2,
weight_kg=95.0 + (2 - i) * 5,
reps=12 - (2 - i),
),
],
)
],
)
await storage.create_session(session)

history = await storage.get_exercise_history("user1", "bench press")

assert len(history) == 3
assert history[0].best_set_weight_kg == 110.0
assert history[0].best_set_reps == 8

@pytest.mark.asyncio
async def test_get_exercise_history_excludes_warmup(self, storage) -> None:
"""Should exclude warmup sets from best set calculation."""
session = WorkoutSession(
user_id="user1",
workout_date="2026-04-26",
split_name="push",
created_at="2026-04-26T10:00:00",
exercises=[
WorkoutExercise(
exercise_name="bench press",
sets=[
SetDetail(set_num=1, weight_kg=60.0, reps=15, is_warmup=True),
SetDetail(set_num=2, weight_kg=100.0, reps=10, is_warmup=False),
],
)
],
)
await storage.create_session(session)

history = await storage.get_exercise_history("user1", "bench press")

assert history[0].best_set_weight_kg == 100.0
assert history[0].best_set_reps == 10

@pytest.mark.asyncio
async def test_delete_session(self, storage) -> None:
"""Should delete a session and cascade to exercises."""
Expand Down Expand Up @@ -502,8 +398,8 @@ async def test_multiple_users_isolated(self, storage) -> None:
assert s1.split_name == "push"
assert s2.split_name == "pull"

sessions1 = await storage.get_recent_sessions("user1")
sessions2 = await storage.get_recent_sessions("user2")
sessions1 = await storage.get_training_history("user1")
sessions2 = await storage.get_training_history("user2")

assert len(sessions1) == 1
assert len(sessions2) == 1
Expand Down
Loading
Loading