diff --git a/.gitea/workflows/refresh_showcase_data.yml b/.gitea/workflows/refresh_showcase_data.yml new file mode 100644 index 0000000..9ac3637 --- /dev/null +++ b/.gitea/workflows/refresh_showcase_data.yml @@ -0,0 +1,41 @@ +name: Refresh Showcase Data + +on: + schedule: + # Run daily at 3 AM UTC + - cron: '0 3 * * *' + workflow_dispatch: # Allow manual triggering + +jobs: + refresh-data: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ vars.PYTHON_VERSION }} + + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + + - name: Install dependencies + run: | + uv sync --no-dev --all-extras + + - name: Run showcase data seeding script + env: + POSTGRES_URI: ${{ secrets.POSTGRES_URI }} + run: | + uv run python scripts/seed_showcase_data.py + + - name: Notify on success + if: success() + run: echo "✅ Showcase data refreshed successfully" + + - name: Notify on failure + if: failure() + run: echo "❌ Failed to refresh showcase data" diff --git a/scripts/seed_showcase_data.py b/scripts/seed_showcase_data.py new file mode 100644 index 0000000..795349a --- /dev/null +++ b/scripts/seed_showcase_data.py @@ -0,0 +1,185 @@ +"""Seed test data into the database for demo purposes.""" + +import sys +import os +from pathlib import Path +from datetime import datetime + +# Add parent directory to path to import baby_monitor modules +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +# Import after path is set up +from baby_monitor.repositories.dependencies.get_database import ( # noqa: E402 + get_database, + init_db, +) +from baby_monitor.repositories.user.sqlite_user import ( # noqa: E402 + DatabaseUserRepository, +) +from baby_monitor.repositories.child.sqlite_child import ( # noqa: E402 + DatabaseChildRepository, +) +from baby_monitor.repositories.feeding.sqlite_feeding import ( # noqa: E402 + DatabaseFeedingRepository, +) +from baby_monitor.repositories.diaper_change.sqlite_diaper_change import ( # noqa: E402, E501 + DatabaseDiaperChangeRepository, +) +from baby_monitor.repositories.sleep.sqlite_sleep import ( # noqa: E402 + DatabaseSleepRepository, +) +from baby_monitor.routers.auth import hash_password # noqa: E402 +from showcase_data_definitions import generate_showcase_data # noqa: E402 + + +def seed_database(reference_time: datetime | None = None) -> None: + """ + Seed the database with test data. + + This function is idempotent - it will check if test users exist + and either create them or update timestamps of existing data. + + Args: + reference_time: The "now" time to use for generating timestamps. + Defaults to current time. + """ + # Initialize database + init_db() + db = next(get_database()) + + try: + # Initialize repositories + user_repo = DatabaseUserRepository(db) + child_repo = DatabaseChildRepository(db) + feeding_repo = DatabaseFeedingRepository(db) + diaper_repo = DatabaseDiaperChangeRepository(db) + sleep_repo = DatabaseSleepRepository(db) + + # Generate showcase data + test_data = generate_showcase_data(reference_time) + + print("Starting database seeding...") + print(f"Reference time: {test_data['reference_time']}") + + # Create or get showcase users + user_ids = [] + for user_data in test_data["users"]: + # Check if user exists + existing_user = user_repo.get_by_username(user_data["username"]) + + if existing_user: + print(f"✓ User '{user_data['username']}' already exists") + user_ids.append(existing_user["id"]) + else: + # Create new user + hashed_pw = hash_password(user_data["password"]) + user_id = user_repo.create( + username=user_data["username"], + hashed_password=hashed_pw, + ) + user_ids.append(user_id["id"]) + print(f"✓ Created user '{user_data['username']}'") + + # Create or get shared child + child_data = test_data["child"] + + # Check if child exists for first user + existing_children = child_repo.get_by_user_id(user_ids[0]) + child_id: int | None = None + + # Look for existing child with matching name + for child in existing_children: + if child["name"] == child_data["name"]: + child_id = child["id"] + print(f"✓ Child '{child_data['name']}' already exists") + break + + if child_id is None: + # Create new child + child_id = child_repo.create( + name=child_data["name"], + birth_time=child_data["birth_time"], + birth_weight=child_data["birth_weight"], + user_id=user_ids[0], + ) + print(f"✓ Created child '{child_data['name']}'") + + # Share child with second user if not already shared + parent_ids = child_repo.get_parent_ids(child_id) + if user_ids[1] not in parent_ids: + child_repo.add_parent(child_id, user_ids[1]) + msg = f"✓ Shared child with '{test_data['users'][1]['username']}'" + print(msg) + + # Ensure child_id is set + assert child_id is not None, "Child ID must be set at this point" + + # Clear existing logs to avoid duplicates + print("Clearing existing logs...") + + # Get all logs for this child + existing_feedings = feeding_repo.get_by_child_id(child_id) + for feeding in existing_feedings: + feeding_repo.delete(feeding["id"]) + + existing_diapers = diaper_repo.get_by_child_id(child_id) + for diaper in existing_diapers: + diaper_repo.delete(diaper["id"]) + + existing_sleep = sleep_repo.get_by_child_id(child_id) + for sleep in existing_sleep: + sleep_repo.delete(sleep["id"]) + + print("✓ Cleared existing logs") + + # Add feeding logs + print(f"Adding {len(test_data['feedings'])} feeding logs...") + for feeding in test_data["feedings"]: + feeding_repo.create( + child_id=child_id, + start_time=feeding["start_time"], + end_time=feeding["end_time"], + feeding_type=feeding["feeding_type"], + ) + print(f"✓ Added {len(test_data['feedings'])} feedings") + + # Add diaper changes + print(f"Adding {len(test_data['diapers'])} diaper changes...") + for diaper in test_data["diapers"]: + diaper_repo.create( + child_id=child_id, + change_time=diaper["time"], + poop_color=diaper.get("poop_color"), + poop_amount=diaper.get("poop_amount"), + pee_color=diaper.get("pee_color"), + pee_amount=diaper.get("pee_amount"), + ) + print(f"✓ Added {len(test_data['diapers'])} diaper changes") + + # Add sleep sessions + print(f"Adding {len(test_data['sleep'])} sleep sessions...") + for sleep in test_data["sleep"]: + sleep_repo.create( + child_id=child_id, + start_time=sleep["start_time"], + end_time=sleep["end_time"], + ) + print(f"✓ Added {len(test_data['sleep'])} sleep sessions") + + db.commit() + print("\n✅ Database seeding completed successfully!") + print("\nTest user credentials:") + print(f" Username: {test_data['users'][0]['username']}") + print(f" Password: {test_data['users'][0]['password']}") + print(" (Both users share the same password)") + + except Exception as e: + db.rollback() + print(f"\n❌ Error seeding database: {e}") + raise + finally: + db.close() + + +if __name__ == "__main__": + seed_database() diff --git a/scripts/showcase_data_definitions.py b/scripts/showcase_data_definitions.py new file mode 100644 index 0000000..ea37a5c --- /dev/null +++ b/scripts/showcase_data_definitions.py @@ -0,0 +1,281 @@ +"""Definition of showcase data to be used for presenting the app.""" + + +from datetime import datetime, timedelta +import random + +# User credentials +SHOWCASE_USERS = [ + { + "username": "user", + "password": "demo123", # Will be hashed when inserted + }, + { + "username": "user2", + "password": "demo123", # Will be hashed when inserted + }, +] + +# Shared child (14 days old) +CHILD_AGE_DAYS = 14 + + +def get_child_data(reference_time: datetime) -> dict: + """Get child data with birth time relative to reference time.""" + birth_time = reference_time - timedelta(days=CHILD_AGE_DAYS) + return { + "name": "Emma Rose", + "birth_time": birth_time, + "birth_weight": 3420, # grams (7.5 lbs) + } + + +def get_feeding_schedule(reference_time: datetime) -> list[dict]: + """ + Generate realistic feeding schedule for the last 14 days. + + Newborns feed every 2-3 hours initially, gradually spacing out + to 3-4 hours. + """ + feedings: list[dict] = [] + current_time = reference_time - timedelta(days=CHILD_AGE_DAYS) + end_time = reference_time + + feeding_types = ["left_breast", "right_breast", "bottle"] + + # Day-by-day feeding patterns + day_number = 0 + while current_time < end_time: + age_in_days = day_number + + # Newborns feed more frequently + if age_in_days < 3: + interval_hours = random.uniform(2, 2.5) + duration_minutes = random.randint(15, 25) + elif age_in_days < 7: + interval_hours = random.uniform(2.5, 3) + duration_minutes = random.randint(20, 30) + else: + interval_hours = random.uniform(3, 3.5) + duration_minutes = random.randint(20, 35) + + # Alternate between breasts and occasional bottle + if random.random() < 0.2: # 20% bottle feeding + feeding_type = "bottle" + else: + # Alternate breasts + feeding_type = feeding_types[len(feedings) % 2] + + start_time = current_time + end_time_feeding = start_time + timedelta(minutes=duration_minutes) + + feedings.append({ + "start_time": start_time, + "end_time": end_time_feeding, + "feeding_type": feeding_type, + }) + + current_time += timedelta(hours=interval_hours) + + # Move to next day + prev_date = ( + current_time - timedelta(hours=interval_hours) + ).date() + if current_time.date() != prev_date: + day_number += 1 + + return feedings + + +def get_diaper_changes(reference_time: datetime) -> list[dict]: + """ + Generate realistic diaper changes for the last 14 days. + + - First 2-3 days: Black/dark (meconium), minimal pee + - Days 3-5: Transition to yellow, more frequent + - Days 5+: Regular yellow stools, normal pee patterns + - 8-10 diaper changes per day initially, settling to 6-8 + """ + diapers = [] + current_time = reference_time - timedelta(days=CHILD_AGE_DAYS) + end_time = reference_time + + day_number = 0 + + while current_time < end_time: + age_in_days = day_number + + # Determine number of diaper changes per day based on age + if age_in_days < 3: + changes_per_day = random.randint(8, 10) + elif age_in_days < 7: + changes_per_day = random.randint(7, 9) + else: + changes_per_day = random.randint(6, 8) + + # Distribute changes throughout the day + day_start = current_time.replace( + hour=0, minute=0, second=0, microsecond=0 + ) + day_end = day_start + timedelta(days=1) + + for _ in range(changes_per_day): + # Random time during the day + random_seconds = random.randint(0, 86400) + change_time = day_start + timedelta(seconds=random_seconds) + + if change_time >= end_time: + break + + # Determine diaper contents + has_poop = random.random() < 0.4 # 40% have poop + # 90% have pee, or always if no poop + has_pee = random.random() < 0.9 or not has_poop + + diaper = { + "time": change_time, + "has_poop": has_poop, + "has_pee": has_pee, + } + + # Poop characteristics based on age + if has_poop: + if age_in_days < 2: + # Meconium phase + diaper["poop_color"] = "black" + diaper["poop_amount"] = "light" + elif age_in_days < 4: + # Transition phase + colors = ["black", "green", "yellow"] + diaper["poop_color"] = random.choice(colors) + amounts = ["light", "medium"] + diaper["poop_amount"] = random.choice(amounts) + else: + # Normal phase + colors = ["yellow", "yellow", "yellow", "green"] + diaper["poop_color"] = random.choice(colors) + # Amount increases with age + if age_in_days < 7: + amounts = ["light", "medium"] + diaper["poop_amount"] = random.choice(amounts) + else: + amounts = ["medium", "heavy"] + diaper["poop_amount"] = random.choice(amounts) + + # Pee characteristics based on age + if has_pee: + if age_in_days < 2: + # Very light pee initially + diaper["pee_amount"] = "light" + diaper["pee_color"] = random.choice(["clear", "yellow"]) + elif age_in_days < 5: + # Increasing output + diaper["pee_amount"] = random.choice(["light", "medium"]) + diaper["pee_color"] = random.choice(["clear", "yellow"]) + else: + # Normal output + if age_in_days < 10: + amounts = ["medium", "medium", "heavy"] + diaper["pee_amount"] = random.choice(amounts) + else: + amounts = ["medium", "heavy", "heavy"] + diaper["pee_amount"] = random.choice(amounts) + colors = ["clear", "yellow", "yellow"] + diaper["pee_color"] = random.choice(colors) + + diapers.append(diaper) + + day_number += 1 + current_time = day_end + + # Sort by time + diapers.sort(key=lambda x: x["time"]) + return diapers + + +def get_sleep_sessions(reference_time: datetime) -> list[dict]: + """ + Generate realistic sleep patterns for the last 14 days. + + Newborns sleep 16-18 hours per day in short bursts (30min - 4 hours). + Sleep gradually consolidates over time. + """ + sleep_sessions = [] + current_time = reference_time - timedelta(days=CHILD_AGE_DAYS) + end_time = reference_time + + day_number = 0 + + while current_time < end_time: + age_in_days = day_number + + # Sleep patterns evolve with age + if age_in_days < 3: + # Very short, frequent naps + avg_wake_time = 1.5 # hours + sleep_duration_range = (30, 120) # minutes + elif age_in_days < 7: + # Slightly longer sleep periods + avg_wake_time = 2 + sleep_duration_range = (45, 180) + else: + # More consolidated sleep + avg_wake_time = 2.5 + sleep_duration_range = (60, 240) + + # Random wake time with variation + wake_minutes = random.randint( + int(avg_wake_time * 45), + int(avg_wake_time * 75) + ) + + # Sleep start time + start_time = current_time + timedelta(minutes=wake_minutes) + + if start_time >= end_time: + break + + # Sleep duration + duration_minutes = random.randint(*sleep_duration_range) + + # Longer sleep at night + if 20 <= start_time.hour or start_time.hour < 6: + duration_minutes = int(duration_minutes * 1.3) + + end_time_sleep = start_time + timedelta(minutes=duration_minutes) + + sleep_sessions.append({ + "start_time": start_time, + "end_time": end_time_sleep, + }) + + current_time = end_time_sleep + birth_time = reference_time - timedelta(days=CHILD_AGE_DAYS) + day_number = (current_time - birth_time).days + + return sleep_sessions + + +def generate_showcase_data(reference_time: datetime | None = None) -> dict: + """ + Generate all test data with timestamps relative to reference time. + + Args: + reference_time: The "now" time to use as reference. + Defaults to current time. + + Returns: + Dictionary containing all test data + """ + if reference_time is None: + reference_time = datetime.now() + + return { + "users": SHOWCASE_USERS, + "child": get_child_data(reference_time), + "feedings": get_feeding_schedule(reference_time), + "diapers": get_diaper_changes(reference_time), + "sleep": get_sleep_sessions(reference_time), + "reference_time": reference_time, + } +