Hp137 commited on
Commit
a1cd5f3
·
1 Parent(s): 80c1e61

feat:Added Steps backend

Browse files
alembic/versions/0875ad9f64a9_add_steps_count_to_water_logs.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """add steps_count to water_logs
2
+
3
+ Revision ID: 0875ad9f64a9
4
+ Revises: 53622d9fbc8d
5
+ Create Date: 2025-12-26 21:45:42.492758
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ import sqlmodel.sql.sqltypes
13
+ from sqlalchemy.dialects import postgresql
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = '0875ad9f64a9'
17
+ down_revision: Union[str, Sequence[str], None] = '53622d9fbc8d'
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ """Upgrade schema."""
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+
26
+ op.add_column('water_logs', sa.Column('steps_count', sa.Integer(), nullable=True))
27
+ op.alter_column('water_logs', 'amount_ml',
28
+ existing_type=sa.INTEGER(),
29
+ nullable=True)
30
+ # ### end Alembic commands ###
31
+
32
+
33
+ def downgrade() -> None:
34
+ """Downgrade schema."""
35
+ # ### commands auto generated by Alembic - please adjust! ###
36
+ op.alter_column('water_logs', 'amount_ml',
37
+ existing_type=sa.INTEGER(),
38
+ nullable=False)
39
+ op.drop_column('water_logs', 'steps_count')
40
+
41
+ # ### end Alembic commands ###
src/wellbeing/models.py CHANGED
@@ -1,18 +1,37 @@
1
- from sqlmodel import Field, SQLModel, Relationship, ForeignKey,Column
 
 
2
  import uuid
3
- from datetime import datetime
4
  from typing import Optional
5
- from sqlalchemy.dialects.postgresql import UUID
6
 
7
  class WaterLogs(SQLModel, table=True):
8
  __tablename__ = "water_logs"
 
9
  id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
 
10
  user_id: uuid.UUID = Field(
11
- sa_column=Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
 
 
 
 
12
  )
13
- amount_ml: int = Field(..., nullable=False)
14
- logged_at: datetime = Field(default_factory=datetime.now, nullable=False)
15
- goal_ml: Optional[int] = Field(default=None, nullable=True)
16
- recommended_ml: Optional[int] = Field(default=None, nullable=True)
17
-
 
 
 
 
 
 
 
 
 
 
 
18
  user: "Users" = Relationship(back_populates="water_logs")
 
1
+ from sqlmodel import Field, SQLModel, Relationship
2
+ from sqlalchemy import Column, ForeignKey
3
+ from sqlalchemy.dialects.postgresql import UUID
4
  import uuid
5
+ from datetime import datetime, timezone
6
  from typing import Optional
7
+
8
 
9
  class WaterLogs(SQLModel, table=True):
10
  __tablename__ = "water_logs"
11
+
12
  id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
13
+
14
  user_id: uuid.UUID = Field(
15
+ sa_column=Column(
16
+ UUID(as_uuid=True),
17
+ ForeignKey("users.id", ondelete="CASCADE"),
18
+ nullable=False,
19
+ )
20
  )
21
+
22
+ # Water (manual)
23
+ amount_ml: Optional[int] = Field(default=None)
24
+
25
+ # Steps (auto)
26
+ steps_count: Optional[int] = Field(default=None)
27
+
28
+ # Single timestamp (UPDATED on every water OR steps update)
29
+ logged_at: datetime = Field(
30
+ default_factory=lambda: datetime.now(timezone.utc), nullable=False
31
+ )
32
+
33
+ # Goals
34
+ goal_ml: Optional[int] = Field(default=None)
35
+ recommended_ml: Optional[int] = Field(default=None)
36
+
37
  user: "Users" = Relationship(back_populates="water_logs")
src/wellbeing/router.py CHANGED
@@ -3,7 +3,13 @@ from src.auth.utils import get_current_user
3
  from fastapi import APIRouter, Depends, HTTPException
4
  from uuid import UUID
5
  from typing import List
6
- from src.wellbeing.schemas import WaterLogCreate, WaterLogUpdate, WaterLog
 
 
 
 
 
 
7
  from . import service
8
  from sqlalchemy.ext.asyncio.session import AsyncSession
9
 
@@ -39,10 +45,10 @@ async def update_water_log(
39
  session: AsyncSession = Depends(get_async_session),
40
  user_id: UUID = Depends(get_current_user),
41
  ):
42
- updated_log = service.update_water_log(session, water_log_id, water_log)
43
  if not updated_log:
44
  raise HTTPException(status_code=404, detail="Water log not found")
45
- return await updated_log
46
 
47
 
48
  # Delete a water log
@@ -56,3 +62,32 @@ async def delete_water_log(
56
  if not success:
57
  raise HTTPException(status_code=404, detail="Water log not found")
58
  return {"message": "Water log deleted successfully"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from fastapi import APIRouter, Depends, HTTPException
4
  from uuid import UUID
5
  from typing import List
6
+ from src.wellbeing.schemas import (
7
+ WaterLogCreate,
8
+ WaterLogUpdate,
9
+ WaterLog,
10
+ StepsLog,
11
+ StepsLogCreate,
12
+ )
13
  from . import service
14
  from sqlalchemy.ext.asyncio.session import AsyncSession
15
 
 
45
  session: AsyncSession = Depends(get_async_session),
46
  user_id: UUID = Depends(get_current_user),
47
  ):
48
+ updated_log = await service.update_water_log(session, water_log_id, water_log)
49
  if not updated_log:
50
  raise HTTPException(status_code=404, detail="Water log not found")
51
+ return updated_log
52
 
53
 
54
  # Delete a water log
 
62
  if not success:
63
  raise HTTPException(status_code=404, detail="Water log not found")
64
  return {"message": "Water log deleted successfully"}
65
+
66
+
67
+ @router.post("/steps", response_model=StepsLog)
68
+ async def create_or_update_steps(
69
+ steps: StepsLogCreate,
70
+ session: AsyncSession = Depends(get_async_session),
71
+ user_id: UUID = Depends(get_current_user),
72
+ ):
73
+ return await service.create_or_update_steps(
74
+ session=session,
75
+ steps=steps,
76
+ user_id=user_id,
77
+ )
78
+
79
+
80
+ @router.get("/getsteps", response_model=list[StepsLog])
81
+ async def get_steps(
82
+ session: AsyncSession = Depends(get_async_session),
83
+ user_id: UUID = Depends(get_current_user),
84
+ ):
85
+ return await service.get_steps_logs(session, user_id)
86
+
87
+ @router.get("/steps/weekly")
88
+ async def get_weekly_steps(
89
+ session: AsyncSession = Depends(get_async_session),
90
+ user_id: UUID = Depends(get_current_user),
91
+ ):
92
+ return await service.get_weekly_steps(session, user_id)
93
+
src/wellbeing/schemas.py CHANGED
@@ -3,17 +3,23 @@ from typing import Optional
3
  import uuid
4
  from datetime import datetime
5
 
 
6
  class WaterLogBase(BaseModel):
7
- amount_ml: int
 
 
8
  goal_ml: Optional[int] = None
9
  recommended_ml: Optional[int] = None
10
 
 
11
  class WaterLogCreate(WaterLogBase):
12
  pass
13
 
 
14
  class WaterLogUpdate(WaterLogBase):
15
  pass
16
 
 
17
  class WaterLog(WaterLogBase):
18
  id: uuid.UUID
19
  user_id: uuid.UUID
@@ -21,3 +27,18 @@ class WaterLog(WaterLogBase):
21
 
22
  model_config = {"from_attributes": True}
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import uuid
4
  from datetime import datetime
5
 
6
+
7
  class WaterLogBase(BaseModel):
8
+
9
+ amount_ml: Optional[int] = None
10
+
11
  goal_ml: Optional[int] = None
12
  recommended_ml: Optional[int] = None
13
 
14
+
15
  class WaterLogCreate(WaterLogBase):
16
  pass
17
 
18
+
19
  class WaterLogUpdate(WaterLogBase):
20
  pass
21
 
22
+
23
  class WaterLog(WaterLogBase):
24
  id: uuid.UUID
25
  user_id: uuid.UUID
 
27
 
28
  model_config = {"from_attributes": True}
29
 
30
+
31
+ class StepsLogBase(BaseModel):
32
+ steps_count: int
33
+
34
+
35
+ class StepsLogCreate(StepsLogBase):
36
+ pass
37
+
38
+
39
+ class StepsLog(StepsLogBase):
40
+ id: uuid.UUID
41
+ user_id: uuid.UUID
42
+ logged_at: datetime
43
+
44
+ model_config = {"from_attributes": True}
src/wellbeing/service.py CHANGED
@@ -1,17 +1,17 @@
1
  from sqlalchemy.ext.asyncio import AsyncSession
2
  from sqlalchemy.future import select
3
- from typing import List
4
  from .models import WaterLogs
5
  import uuid
6
- from .schemas import WaterLogCreate, WaterLogUpdate
7
- from datetime import date, datetime
 
8
 
9
 
10
  # Create or update today's water log
11
  async def create_water_log(
12
  session: AsyncSession, water_log: WaterLogCreate, user_id: uuid.UUID
13
  ) -> WaterLogs:
14
-
15
 
16
  today = date.today()
17
 
@@ -29,6 +29,7 @@ async def create_water_log(
29
  existing_log.amount_ml = water_log.amount_ml
30
  existing_log.goal_ml = water_log.goal_ml
31
  existing_log.recommended_ml = water_log.recommended_ml
 
32
  await session.commit()
33
  await session.refresh(existing_log)
34
  return existing_log
@@ -71,6 +72,7 @@ async def update_water_log(
71
  db_water_log.amount_ml = water_log.amount_ml
72
  db_water_log.goal_ml = water_log.goal_ml
73
  db_water_log.recommended_ml = water_log.recommended_ml
 
74
  await session.commit() # Commit asynchronously
75
  await session.refresh(db_water_log) # Refresh asynchronously
76
  return db_water_log
@@ -88,3 +90,115 @@ async def delete_water_log(session: AsyncSession, water_log_id: uuid.UUID) -> bo
88
  await session.commit() # Commit asynchronously
89
  return True
90
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from sqlalchemy.ext.asyncio import AsyncSession
2
  from sqlalchemy.future import select
3
+ from typing import List , Dict
4
  from .models import WaterLogs
5
  import uuid
6
+ from .schemas import WaterLogCreate, WaterLogUpdate, StepsLogCreate
7
+ from datetime import date, datetime , timedelta
8
+
9
 
10
 
11
  # Create or update today's water log
12
  async def create_water_log(
13
  session: AsyncSession, water_log: WaterLogCreate, user_id: uuid.UUID
14
  ) -> WaterLogs:
 
15
 
16
  today = date.today()
17
 
 
29
  existing_log.amount_ml = water_log.amount_ml
30
  existing_log.goal_ml = water_log.goal_ml
31
  existing_log.recommended_ml = water_log.recommended_ml
32
+ existing_log.logged_at = datetime.utcnow()
33
  await session.commit()
34
  await session.refresh(existing_log)
35
  return existing_log
 
72
  db_water_log.amount_ml = water_log.amount_ml
73
  db_water_log.goal_ml = water_log.goal_ml
74
  db_water_log.recommended_ml = water_log.recommended_ml
75
+ db_water_log.logged_at = datetime.utcnow()
76
  await session.commit() # Commit asynchronously
77
  await session.refresh(db_water_log) # Refresh asynchronously
78
  return db_water_log
 
90
  await session.commit() # Commit asynchronously
91
  return True
92
  return False
93
+
94
+
95
+ async def create_or_update_steps(
96
+ session: AsyncSession,
97
+ steps: StepsLogCreate,
98
+ user_id: uuid.UUID,
99
+ ) -> WaterLogs:
100
+
101
+ today = date.today()
102
+
103
+ stmt = select(WaterLogs).filter(
104
+ WaterLogs.user_id == user_id,
105
+ WaterLogs.logged_at >= datetime.combine(today, datetime.min.time()),
106
+ WaterLogs.logged_at <= datetime.combine(today, datetime.max.time()),
107
+ )
108
+
109
+ result = await session.execute(stmt)
110
+ existing_log = result.scalar_one_or_none()
111
+
112
+ # UPDATE existing row
113
+ if existing_log:
114
+ existing_log.steps_count = steps.steps_count # overwrite
115
+ existing_log.logged_at = datetime.utcnow()
116
+ await session.commit()
117
+ await session.refresh(existing_log)
118
+ return existing_log
119
+
120
+ # CREATE new row
121
+ new_log = WaterLogs(
122
+ id=uuid.uuid4(),
123
+ user_id=user_id,
124
+ steps_count=steps.steps_count,
125
+ logged_at=datetime.utcnow(),
126
+ )
127
+
128
+ session.add(new_log)
129
+ await session.commit()
130
+ await session.refresh(new_log)
131
+ return new_log
132
+
133
+ async def get_steps_logs(
134
+ session: AsyncSession,
135
+ user_id: uuid.UUID,
136
+ skip: int = 0,
137
+ limit: int = 30,
138
+ ) -> List[WaterLogs]:
139
+ """
140
+ Get step logs for a user (latest first)
141
+ """
142
+
143
+ stmt = (
144
+ select(WaterLogs)
145
+ .filter(
146
+ WaterLogs.user_id == user_id,
147
+ WaterLogs.steps_count.isnot(None),
148
+ )
149
+ .order_by(WaterLogs.logged_at.desc())
150
+ .offset(skip)
151
+ .limit(limit)
152
+ )
153
+
154
+ result = await session.execute(stmt)
155
+ return result.scalars().all()
156
+
157
+
158
+
159
+
160
+
161
+
162
+ async def get_weekly_steps(
163
+ session: AsyncSession,
164
+ user_id: uuid.UUID,
165
+ ) -> Dict[str, int]:
166
+ """
167
+ Returns steps grouped by weekday for current week
168
+ Example:
169
+ {
170
+ "Mon": 3200,
171
+ "Tue": 4500,
172
+ ...
173
+ }
174
+ """
175
+
176
+ today = date.today()
177
+ start_of_week = today - timedelta(days=today.weekday()) # Monday
178
+ end_of_week = start_of_week + timedelta(days=6)
179
+
180
+ stmt = select(WaterLogs).filter(
181
+ WaterLogs.user_id == user_id,
182
+ WaterLogs.steps_count.isnot(None),
183
+ WaterLogs.logged_at >= start_of_week,
184
+ WaterLogs.logged_at <= end_of_week,
185
+ )
186
+
187
+ result = await session.execute(stmt)
188
+ logs = result.scalars().all()
189
+
190
+ week_map = {
191
+ "Mon": 0,
192
+ "Tue": 0,
193
+ "Wed": 0,
194
+ "Thu": 0,
195
+ "Fri": 0,
196
+ "Sat": 0,
197
+ "Sun": 0,
198
+ }
199
+
200
+ for log in logs:
201
+ day = log.logged_at.strftime("%a")
202
+ week_map[day] = log.steps_count
203
+
204
+ return week_map