This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
from fastapi import FastAPI, HTTPException, Depends, Query
|
||||
from datetime import date, datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import and_, desc, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_, desc
|
||||
from shared.config import settings
|
||||
from shared.database import get_db
|
||||
|
||||
from services.calendar_service.models import CalendarEntry, CycleData, HealthInsights
|
||||
from services.user_service.main import get_current_user
|
||||
from services.user_service.models import User
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, date, timedelta
|
||||
from enum import Enum
|
||||
from shared.config import settings
|
||||
from shared.database import get_db
|
||||
|
||||
app = FastAPI(title="Calendar Service", version="1.0.0")
|
||||
|
||||
@@ -79,7 +81,7 @@ class CalendarEntryResponse(BaseModel):
|
||||
is_predicted: bool
|
||||
confidence_score: Optional[int]
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -95,7 +97,7 @@ class CycleDataResponse(BaseModel):
|
||||
next_period_predicted: Optional[date]
|
||||
avg_cycle_length: Optional[int]
|
||||
avg_period_length: Optional[int]
|
||||
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -108,7 +110,7 @@ class HealthInsightResponse(BaseModel):
|
||||
recommendation: Optional[str]
|
||||
confidence_level: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -122,10 +124,12 @@ class CycleOverview(BaseModel):
|
||||
avg_cycle_length: Optional[int]
|
||||
|
||||
|
||||
def calculate_cycle_phase(cycle_start: date, cycle_length: int, current_date: date) -> str:
|
||||
def calculate_cycle_phase(
|
||||
cycle_start: date, cycle_length: int, current_date: date
|
||||
) -> str:
|
||||
"""Calculate current cycle phase"""
|
||||
days_since_start = (current_date - cycle_start).days
|
||||
|
||||
|
||||
if days_since_start <= 5:
|
||||
return "menstrual"
|
||||
elif days_since_start <= cycle_length // 2 - 2:
|
||||
@@ -146,29 +150,30 @@ async def calculate_predictions(user_id: int, db: AsyncSession):
|
||||
.limit(6)
|
||||
)
|
||||
cycle_list = cycles.scalars().all()
|
||||
|
||||
|
||||
if len(cycle_list) < 2:
|
||||
return None
|
||||
|
||||
|
||||
# Calculate averages
|
||||
cycle_lengths = [c.cycle_length for c in cycle_list if c.cycle_length]
|
||||
period_lengths = [c.period_length for c in cycle_list if c.period_length]
|
||||
|
||||
|
||||
if not cycle_lengths:
|
||||
return None
|
||||
|
||||
|
||||
avg_cycle = sum(cycle_lengths) / len(cycle_lengths)
|
||||
avg_period = sum(period_lengths) / len(period_lengths) if period_lengths else 5
|
||||
|
||||
|
||||
# Predict next period
|
||||
last_cycle = cycle_list[0]
|
||||
next_period_date = last_cycle.cycle_start_date + timedelta(days=int(avg_cycle))
|
||||
|
||||
|
||||
return {
|
||||
"avg_cycle_length": int(avg_cycle),
|
||||
"avg_period_length": int(avg_period),
|
||||
"next_period_predicted": next_period_date,
|
||||
"ovulation_date": last_cycle.cycle_start_date + timedelta(days=int(avg_cycle // 2))
|
||||
"ovulation_date": last_cycle.cycle_start_date
|
||||
+ timedelta(days=int(avg_cycle // 2)),
|
||||
}
|
||||
|
||||
|
||||
@@ -176,31 +181,32 @@ async def calculate_predictions(user_id: int, db: AsyncSession):
|
||||
async def create_calendar_entry(
|
||||
entry_data: CalendarEntryCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Create new calendar entry"""
|
||||
|
||||
|
||||
# Check if entry already exists for this date and type
|
||||
existing = await db.execute(
|
||||
select(CalendarEntry).filter(
|
||||
and_(
|
||||
CalendarEntry.user_id == current_user.id,
|
||||
CalendarEntry.entry_date == entry_data.entry_date,
|
||||
CalendarEntry.entry_type == entry_data.entry_type.value
|
||||
CalendarEntry.entry_type == entry_data.entry_type.value,
|
||||
)
|
||||
)
|
||||
)
|
||||
if existing.scalars().first():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Entry already exists for this date and type"
|
||||
status_code=400, detail="Entry already exists for this date and type"
|
||||
)
|
||||
|
||||
|
||||
db_entry = CalendarEntry(
|
||||
user_id=current_user.id,
|
||||
entry_date=entry_data.entry_date,
|
||||
entry_type=entry_data.entry_type.value,
|
||||
flow_intensity=entry_data.flow_intensity.value if entry_data.flow_intensity else None,
|
||||
flow_intensity=entry_data.flow_intensity.value
|
||||
if entry_data.flow_intensity
|
||||
else None,
|
||||
period_symptoms=entry_data.period_symptoms,
|
||||
mood=entry_data.mood.value if entry_data.mood else None,
|
||||
energy_level=entry_data.energy_level,
|
||||
@@ -209,21 +215,21 @@ async def create_calendar_entry(
|
||||
medications=entry_data.medications,
|
||||
notes=entry_data.notes,
|
||||
)
|
||||
|
||||
|
||||
db.add(db_entry)
|
||||
await db.commit()
|
||||
await db.refresh(db_entry)
|
||||
|
||||
|
||||
# If this is a period entry, update cycle data
|
||||
if entry_data.entry_type == EntryType.PERIOD:
|
||||
await update_cycle_data(current_user.id, entry_data.entry_date, db)
|
||||
|
||||
|
||||
return CalendarEntryResponse.model_validate(db_entry)
|
||||
|
||||
|
||||
async def update_cycle_data(user_id: int, period_date: date, db: AsyncSession):
|
||||
"""Update cycle data when period is logged"""
|
||||
|
||||
|
||||
# Get last cycle
|
||||
last_cycle = await db.execute(
|
||||
select(CycleData)
|
||||
@@ -232,23 +238,25 @@ async def update_cycle_data(user_id: int, period_date: date, db: AsyncSession):
|
||||
.limit(1)
|
||||
)
|
||||
last_cycle_data = last_cycle.scalars().first()
|
||||
|
||||
|
||||
if last_cycle_data:
|
||||
# Calculate cycle length
|
||||
cycle_length = (period_date - last_cycle_data.cycle_start_date).days
|
||||
last_cycle_data.cycle_length = cycle_length
|
||||
|
||||
|
||||
# Create new cycle
|
||||
predictions = await calculate_predictions(user_id, db)
|
||||
|
||||
|
||||
new_cycle = CycleData(
|
||||
user_id=user_id,
|
||||
cycle_start_date=period_date,
|
||||
avg_cycle_length=predictions["avg_cycle_length"] if predictions else None,
|
||||
next_period_predicted=predictions["next_period_predicted"] if predictions else None,
|
||||
next_period_predicted=predictions["next_period_predicted"]
|
||||
if predictions
|
||||
else None,
|
||||
ovulation_date=predictions["ovulation_date"] if predictions else None,
|
||||
)
|
||||
|
||||
|
||||
db.add(new_cycle)
|
||||
await db.commit()
|
||||
|
||||
@@ -260,34 +268,33 @@ async def get_calendar_entries(
|
||||
start_date: Optional[date] = Query(None),
|
||||
end_date: Optional[date] = Query(None),
|
||||
entry_type: Optional[EntryType] = Query(None),
|
||||
limit: int = Query(100, ge=1, le=365)
|
||||
limit: int = Query(100, ge=1, le=365),
|
||||
):
|
||||
"""Get calendar entries with optional filtering"""
|
||||
|
||||
|
||||
query = select(CalendarEntry).filter(CalendarEntry.user_id == current_user.id)
|
||||
|
||||
|
||||
if start_date:
|
||||
query = query.filter(CalendarEntry.entry_date >= start_date)
|
||||
if end_date:
|
||||
query = query.filter(CalendarEntry.entry_date <= end_date)
|
||||
if entry_type:
|
||||
query = query.filter(CalendarEntry.entry_type == entry_type.value)
|
||||
|
||||
|
||||
query = query.order_by(desc(CalendarEntry.entry_date)).limit(limit)
|
||||
|
||||
|
||||
result = await db.execute(query)
|
||||
entries = result.scalars().all()
|
||||
|
||||
|
||||
return [CalendarEntryResponse.model_validate(entry) for entry in entries]
|
||||
|
||||
|
||||
@app.get("/api/v1/cycle-overview", response_model=CycleOverview)
|
||||
async def get_cycle_overview(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get current cycle overview and predictions"""
|
||||
|
||||
|
||||
# Get current cycle
|
||||
current_cycle = await db.execute(
|
||||
select(CycleData)
|
||||
@@ -296,7 +303,7 @@ async def get_cycle_overview(
|
||||
.limit(1)
|
||||
)
|
||||
cycle_data = current_cycle.scalars().first()
|
||||
|
||||
|
||||
if not cycle_data:
|
||||
return CycleOverview(
|
||||
current_cycle_day=None,
|
||||
@@ -304,22 +311,24 @@ async def get_cycle_overview(
|
||||
next_period_date=None,
|
||||
days_until_period=None,
|
||||
cycle_regularity="unknown",
|
||||
avg_cycle_length=None
|
||||
avg_cycle_length=None,
|
||||
)
|
||||
|
||||
|
||||
today = date.today()
|
||||
current_cycle_day = (today - cycle_data.cycle_start_date).days + 1
|
||||
|
||||
|
||||
# Calculate current phase
|
||||
cycle_length = cycle_data.avg_cycle_length or 28
|
||||
current_phase = calculate_cycle_phase(cycle_data.cycle_start_date, cycle_length, today)
|
||||
|
||||
current_phase = calculate_cycle_phase(
|
||||
cycle_data.cycle_start_date, cycle_length, today
|
||||
)
|
||||
|
||||
# Days until next period
|
||||
next_period_date = cycle_data.next_period_predicted
|
||||
days_until_period = None
|
||||
if next_period_date:
|
||||
days_until_period = (next_period_date - today).days
|
||||
|
||||
|
||||
# Calculate regularity
|
||||
cycles = await db.execute(
|
||||
select(CycleData)
|
||||
@@ -328,7 +337,7 @@ async def get_cycle_overview(
|
||||
.limit(6)
|
||||
)
|
||||
cycle_list = cycles.scalars().all()
|
||||
|
||||
|
||||
regularity = "unknown"
|
||||
if len(cycle_list) >= 3:
|
||||
lengths = [c.cycle_length for c in cycle_list if c.cycle_length]
|
||||
@@ -342,14 +351,14 @@ async def get_cycle_overview(
|
||||
regularity = "irregular"
|
||||
else:
|
||||
regularity = "very_irregular"
|
||||
|
||||
|
||||
return CycleOverview(
|
||||
current_cycle_day=current_cycle_day,
|
||||
current_phase=current_phase,
|
||||
next_period_date=next_period_date,
|
||||
days_until_period=days_until_period,
|
||||
cycle_regularity=regularity,
|
||||
avg_cycle_length=cycle_data.avg_cycle_length
|
||||
avg_cycle_length=cycle_data.avg_cycle_length,
|
||||
)
|
||||
|
||||
|
||||
@@ -357,21 +366,21 @@ async def get_cycle_overview(
|
||||
async def get_health_insights(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
limit: int = Query(10, ge=1, le=50)
|
||||
limit: int = Query(10, ge=1, le=50),
|
||||
):
|
||||
"""Get personalized health insights"""
|
||||
|
||||
|
||||
result = await db.execute(
|
||||
select(HealthInsights)
|
||||
.filter(
|
||||
HealthInsights.user_id == current_user.id,
|
||||
HealthInsights.is_dismissed == False
|
||||
HealthInsights.is_dismissed == False,
|
||||
)
|
||||
.order_by(desc(HealthInsights.created_at))
|
||||
.limit(limit)
|
||||
)
|
||||
insights = result.scalars().all()
|
||||
|
||||
|
||||
return [HealthInsightResponse.model_validate(insight) for insight in insights]
|
||||
|
||||
|
||||
@@ -379,26 +388,23 @@ async def get_health_insights(
|
||||
async def delete_calendar_entry(
|
||||
entry_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Delete calendar entry"""
|
||||
|
||||
|
||||
result = await db.execute(
|
||||
select(CalendarEntry).filter(
|
||||
and_(
|
||||
CalendarEntry.id == entry_id,
|
||||
CalendarEntry.user_id == current_user.id
|
||||
)
|
||||
and_(CalendarEntry.id == entry_id, CalendarEntry.user_id == current_user.id)
|
||||
)
|
||||
)
|
||||
entry = result.scalars().first()
|
||||
|
||||
|
||||
if not entry:
|
||||
raise HTTPException(status_code=404, detail="Entry not found")
|
||||
|
||||
|
||||
await db.delete(entry)
|
||||
await db.commit()
|
||||
|
||||
|
||||
return {"message": "Entry deleted successfully"}
|
||||
|
||||
|
||||
@@ -410,4 +416,5 @@ async def health_check():
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8004)
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8004)
|
||||
|
||||
Reference in New Issue
Block a user