pipeline issues fix
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-09-25 11:59:54 +09:00
parent dc50a9858e
commit 4e3768a6ee
39 changed files with 1297 additions and 739 deletions

View File

@@ -0,0 +1 @@
# Calendar Service Package

View File

@@ -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)

View File

@@ -1,77 +1,83 @@
from sqlalchemy import Column, String, Integer, Date, Text, Boolean, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from shared.database import BaseModel
import uuid
from sqlalchemy import Boolean, Column, Date, ForeignKey, Integer, String, Text
from sqlalchemy.dialects.postgresql import UUID
from shared.database import BaseModel
class CalendarEntry(BaseModel):
__tablename__ = "calendar_entries"
uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
entry_date = Column(Date, nullable=False, index=True)
entry_type = Column(String(50), nullable=False) # period, ovulation, symptoms, medication, etc.
entry_type = Column(
String(50), nullable=False
) # period, ovulation, symptoms, medication, etc.
# Period tracking
flow_intensity = Column(String(20)) # light, medium, heavy
period_symptoms = Column(Text) # cramps, headache, mood, etc.
# General health
mood = Column(String(20)) # happy, sad, anxious, irritated, etc.
energy_level = Column(Integer) # 1-5 scale
sleep_hours = Column(Integer)
# Symptoms and notes
symptoms = Column(Text) # Any symptoms experienced
medications = Column(Text) # Medications taken
notes = Column(Text) # Personal notes
# Predictions and calculations
is_predicted = Column(Boolean, default=False) # If this is a predicted entry
confidence_score = Column(Integer) # Prediction confidence 1-100
def __repr__(self):
return f"<CalendarEntry user_id={self.user_id} date={self.entry_date} type={self.entry_type}>"
class CycleData(BaseModel):
__tablename__ = "cycle_data"
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
cycle_start_date = Column(Date, nullable=False)
cycle_length = Column(Integer) # Length of this cycle
period_length = Column(Integer) # Length of period in this cycle
# Calculated fields
ovulation_date = Column(Date)
fertile_window_start = Column(Date)
fertile_window_end = Column(Date)
next_period_predicted = Column(Date)
# Cycle characteristics
cycle_regularity_score = Column(Integer) # 1-100, how regular is this cycle
avg_cycle_length = Column(Integer) # Rolling average
avg_period_length = Column(Integer) # Rolling average
def __repr__(self):
return f"<CycleData user_id={self.user_id} start={self.cycle_start_date}>"
class HealthInsights(BaseModel):
__tablename__ = "health_insights"
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
insight_type = Column(String(50), nullable=False) # cycle_pattern, symptom_pattern, etc.
insight_type = Column(
String(50), nullable=False
) # cycle_pattern, symptom_pattern, etc.
title = Column(String(200), nullable=False)
description = Column(Text, nullable=False)
recommendation = Column(Text)
# Metadata
confidence_level = Column(String(20)) # high, medium, low
data_points_used = Column(Integer) # How many data points were used
is_dismissed = Column(Boolean, default=False)
def __repr__(self):
return f"<HealthInsights user_id={self.user_id} type={self.insight_type}>"
return f"<HealthInsights user_id={self.user_id} type={self.insight_type}>"