diff --git a/Dockerfile b/Dockerfile index f3dbdf8..e851a47 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,9 @@ COPY pyproject.toml README.md ./ COPY src/ ./src/ # Install Python dependencies including all optional dependencies -# This makes the image self-contained and ready for any configuration +# This makes the image self-contained and ready for any configuration: +# - Redis support (redis extra) +# - PostgreSQL support (postgres extra) RUN uv sync --no-dev --all-extras # Add virtual environment to PATH diff --git a/README.md b/README.md index 24122e0..4eb8eb4 100644 --- a/README.md +++ b/README.md @@ -81,13 +81,14 @@ uv run uvicorn src.baby_monitor.main:app --reload --host 0.0.0.0 --port 8000 ### Environment Variables -| Variable | Default | Description | -| ---------------- | ------------ | ------------------------------------------- | -| `ENVIRONMENT` | `production` | Set to `development` to enable API docs | -| `ADMIN_PASSWORD` | _(required)_ | Admin user password | -| `ADMIN_USERNAME` | `admin` | Admin username | -| `DATA_DIR` | `/data` | Directory for SQLite database | -| `REDIS_URI` | _(optional)_ | Redis connection URI for distributed tokens | +| Variable | Default | Description | +| ---------------- | ------------ | ---------------------------------------------- | +| `ENVIRONMENT` | `production` | Set to `development` to enable API docs | +| `ADMIN_PASSWORD` | _(required)_ | Admin user password | +| `ADMIN_USERNAME` | `admin` | Admin username | +| `DATA_DIR` | `/data` | Directory for SQLite database (SQLite only) | +| `REDIS_URI` | _(optional)_ | Redis connection URI for distributed tokens | +| `POSTGRES_URI` | _(optional)_ | PostgreSQL connection URI for database storage | ### Storage Options @@ -96,18 +97,26 @@ uv run uvicorn src.baby_monitor.main:app --reload --host 0.0.0.0 --port 8000 - Automatic setup, no configuration needed - Data stored in `/data/baby_monitor.db` - Perfect for single-server deployments +- Timezone-aware datetimes stored as naive UTC -**Redis (Optional)** +**Redis (Optional - Token Storage)** ```bash -# Enable Redis token storage +# Enable Redis token storage for distributed deployments export REDIS_URI=redis://localhost:6379 ``` -**PostgreSQL (Future)** +**PostgreSQL (Optional - Database)** -- Repository interface ready -- Swap implementation in `dependencies.py` +```bash +# Enable PostgreSQL for production database +export POSTGRES_URI=postgresql://user:password@localhost:5432/baby_monitor +``` + +- Supports timezone-aware datetimes natively +- Recommended for production deployments +- Better performance for concurrent access +- Supports database migrations and backups ## 📁 Project Structure diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 5b074b2..764f5d8 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -5,9 +5,6 @@ services: dockerfile: Dockerfile ports: - "8000:8000" - volumes: - # Persist database to local directory - - ./data:/data environment: # Set environment to production - ENVIRONMENT=production @@ -17,7 +14,7 @@ services: # Use Redis for token storage - REDIS_URI=redis://redis:6379/0 # Use PostgreSQL for database - - DATABASE_URL=postgresql://baby_monitor:securepassword@postgres:5432/baby_monitor_db + - POSTGRES_URI=postgresql://baby_monitor:securepassword@postgres:5432/baby_monitor_db command: ["--host", "0.0.0.0", "--port", "8000"] restart: unless-stopped depends_on: diff --git a/src/baby_monitor/models/db/child.py b/src/baby_monitor/models/db/child.py index 3c7a408..ead31a3 100644 --- a/src/baby_monitor/models/db/child.py +++ b/src/baby_monitor/models/db/child.py @@ -19,9 +19,11 @@ class Child(Base): id = Column(Integer, primary_key=True, index=True) name = Column(String, nullable=False) - birth_time = Column(DateTime, nullable=False) + birth_time = Column(DateTime(timezone=True), nullable=False) birth_weight = Column(Float, nullable=False) # in grams - created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + created_at = Column( + DateTime(timezone=True), default=datetime.utcnow, nullable=False + ) def __repr__(self) -> str: return f"" diff --git a/src/baby_monitor/models/db/child_parent.py b/src/baby_monitor/models/db/child_parent.py index 1d0bc6e..cf50e06 100644 --- a/src/baby_monitor/models/db/child_parent.py +++ b/src/baby_monitor/models/db/child_parent.py @@ -20,7 +20,9 @@ class ChildParent(Base): id = Column(Integer, primary_key=True, index=True) child_id = Column(Integer, ForeignKey("children.id"), nullable=False) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) - created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + created_at = Column( + DateTime(timezone=True), default=datetime.utcnow, nullable=False + ) def __repr__(self) -> str: return ( diff --git a/src/baby_monitor/models/db/diaper_change.py b/src/baby_monitor/models/db/diaper_change.py index 66daa56..ddf32f9 100644 --- a/src/baby_monitor/models/db/diaper_change.py +++ b/src/baby_monitor/models/db/diaper_change.py @@ -19,12 +19,14 @@ class DiaperChange(Base): id = Column(Integer, primary_key=True, index=True) child_id = Column(Integer, ForeignKey("children.id"), nullable=False) - change_time = Column(DateTime, nullable=False) + change_time = Column(DateTime(timezone=True), nullable=False) poop_amount = Column(String, nullable=True) # stores PoopAmount enum poop_color = Column(String, nullable=True) # stores PoopColor enum pee_amount = Column(String, nullable=True) # stores PeeAmount enum pee_color = Column(String, nullable=True) # stores PeeColor enum - created_at = Column(DateTime, default=datetime.now(UTC), nullable=False) + created_at = Column( + DateTime(timezone=True), default=datetime.now(UTC), nullable=False + ) def __repr__(self) -> str: return f"" diff --git a/src/baby_monitor/models/db/feeding.py b/src/baby_monitor/models/db/feeding.py index 60d8c7c..48296d4 100644 --- a/src/baby_monitor/models/db/feeding.py +++ b/src/baby_monitor/models/db/feeding.py @@ -19,10 +19,12 @@ class Feeding(Base): id = Column(Integer, primary_key=True, index=True) child_id = Column(Integer, ForeignKey("children.id"), nullable=False) - start_time = Column(DateTime, nullable=False) - end_time = Column(DateTime, nullable=True) + start_time = Column(DateTime(timezone=True), nullable=False) + end_time = Column(DateTime(timezone=True), nullable=True) feeding_type = Column(String, nullable=False) # stores FeedingType enum - created_at = Column(DateTime, default=datetime.now(UTC), nullable=False) + created_at = Column( + DateTime(timezone=True), default=datetime.now(UTC), nullable=False + ) def __repr__(self) -> str: return f"" diff --git a/src/baby_monitor/models/db/sleep.py b/src/baby_monitor/models/db/sleep.py index 0b99521..fb43199 100644 --- a/src/baby_monitor/models/db/sleep.py +++ b/src/baby_monitor/models/db/sleep.py @@ -1,8 +1,8 @@ """Sleep database model.""" from typing import TYPE_CHECKING -from sqlalchemy import Column, Integer, DateTime, ForeignKey from datetime import datetime, UTC +from sqlalchemy import Column, Integer, DateTime, ForeignKey if TYPE_CHECKING: from sqlalchemy.orm import DeclarativeBase @@ -19,9 +19,11 @@ class Sleep(Base): id = Column(Integer, primary_key=True, index=True) child_id = Column(Integer, ForeignKey("children.id"), nullable=False) - start_time = Column(DateTime, nullable=False) - end_time = Column(DateTime, nullable=True) - created_at = Column(DateTime, default=datetime.now(UTC), nullable=False) + start_time = Column(DateTime(timezone=True), nullable=False) + end_time = Column(DateTime(timezone=True), nullable=True) + created_at = Column( + DateTime(timezone=True), default=datetime.now(UTC), nullable=False + ) def __repr__(self) -> str: return f"" diff --git a/src/baby_monitor/models/db/user.py b/src/baby_monitor/models/db/user.py index c4f4be2..71252a0 100644 --- a/src/baby_monitor/models/db/user.py +++ b/src/baby_monitor/models/db/user.py @@ -21,7 +21,9 @@ class User(Base): username = Column(String, unique=True, index=True, nullable=False) hashed_password = Column(String, nullable=False) is_admin = Column(Boolean, default=False, nullable=False) - created_at = Column(DateTime, default=datetime.utcnow, nullable=False) + created_at = Column( + DateTime(timezone=True), default=datetime.utcnow, nullable=False + ) def __repr__(self) -> str: return f"" diff --git a/src/baby_monitor/repositories/child/sqlite_child.py b/src/baby_monitor/repositories/child/sqlite_child.py index 4c4c3b8..ed0fc3f 100644 --- a/src/baby_monitor/repositories/child/sqlite_child.py +++ b/src/baby_monitor/repositories/child/sqlite_child.py @@ -1,17 +1,23 @@ -"""SQLite implementation of child repository.""" +"""Database-agnostic implementation of child repository. + +Works with both SQLite and PostgreSQL. +""" from datetime import datetime, UTC from sqlalchemy.orm import Session -from baby_monitor.repositories.interfaces.child_repository_interface import ( +from baby_monitor.repositories.interfaces import ( ChildRepositoryInterface, ) from baby_monitor.models.db.child import Child from baby_monitor.models.db.child_parent import ChildParent -class SQLiteChildRepository(ChildRepositoryInterface): - """SQLite implementation for child data access.""" +class DatabaseChildRepository(ChildRepositoryInterface): + """Database implementation for child data access. + + Works with both SQLite and PostgreSQL databases. + """ def __init__(self, db: Session): self.db = db diff --git a/src/baby_monitor/repositories/child_invitation/__init__.py b/src/baby_monitor/repositories/child_invitation/__init__.py index 0ef7a5f..49b26ac 100644 --- a/src/baby_monitor/repositories/child_invitation/__init__.py +++ b/src/baby_monitor/repositories/child_invitation/__init__.py @@ -1,7 +1,13 @@ """Child invitation repository implementations.""" from baby_monitor.repositories.child_invitation.sqlite_child_invitation import ( # noqa: E501 - SQLiteChildInvitationRepository, + DatabaseChildInvitationRepository, ) -__all__ = ["SQLiteChildInvitationRepository"] +# Keep old name for backward compatibility +SQLiteChildInvitationRepository = DatabaseChildInvitationRepository + +__all__ = [ + "DatabaseChildInvitationRepository", + "SQLiteChildInvitationRepository", +] diff --git a/src/baby_monitor/repositories/child_invitation/sqlite_child_invitation.py b/src/baby_monitor/repositories/child_invitation/sqlite_child_invitation.py index 0371999..30bb674 100644 --- a/src/baby_monitor/repositories/child_invitation/sqlite_child_invitation.py +++ b/src/baby_monitor/repositories/child_invitation/sqlite_child_invitation.py @@ -1,20 +1,45 @@ -"""SQLite implementation of child invitation repository.""" +"""Database-agnostic implementation of child invitation repository. + +Supports both SQLite and PostgreSQL: +- SQLite: stores datetimes as naive UTC +- PostgreSQL: stores datetimes as timezone-aware +""" from datetime import datetime, UTC from sqlalchemy.orm import Session from baby_monitor.models.child_invitation import ChildInvitation -from baby_monitor.repositories.interfaces.child_invitation import ( +from baby_monitor.repositories.interfaces import ( ChildInvitationRepositoryInterface, ) -class SQLiteChildInvitationRepository(ChildInvitationRepositoryInterface): - """SQLite implementation of child invitation repository.""" +class DatabaseChildInvitationRepository(ChildInvitationRepositoryInterface): + """Database implementation of child invitation repository. + + Works with both SQLite and PostgreSQL databases. + """ def __init__(self, db: Session): """Initialize the repository with a database session.""" self.db = db + def _get_now_utc(self, reference_dt: datetime) -> datetime: + """ + Get current UTC time matching the timezone awareness of reference. + + Args: + reference_dt: A datetime from DB to match timezone format + + Returns: + Current UTC time (naive for SQLite, aware for PostgreSQL) + """ + now = datetime.now(UTC) + if reference_dt.tzinfo is None: + # SQLite: return naive UTC + return now.replace(tzinfo=None) + # PostgreSQL: return timezone-aware + return now + def create_invitation( self, code: str, @@ -68,8 +93,8 @@ class SQLiteChildInvitationRepository(ChildInvitationRepositoryInterface): if invitation.is_consumed: return False - # Compare as naive UTC datetimes (SQLite stores without timezone) - now_utc = datetime.now(UTC).replace(tzinfo=None) + # Get current time matching database timezone format + now_utc = self._get_now_utc(invitation.expires_at) if invitation.expires_at < now_utc: return False diff --git a/src/baby_monitor/repositories/dependencies/get_child_invitation_repository.py b/src/baby_monitor/repositories/dependencies/get_child_invitation_repository.py index 76ffad7..d5c73f7 100644 --- a/src/baby_monitor/repositories/dependencies/get_child_invitation_repository.py +++ b/src/baby_monitor/repositories/dependencies/get_child_invitation_repository.py @@ -4,13 +4,16 @@ from typing import Annotated from fastapi import Depends from sqlalchemy.orm import Session from baby_monitor.repositories.child_invitation.sqlite_child_invitation import ( # noqa: E501 - SQLiteChildInvitationRepository, + DatabaseChildInvitationRepository, +) +from baby_monitor.repositories.interfaces import ( + ChildInvitationRepositoryInterface, ) from baby_monitor.repositories.dependencies.get_database import get_database def get_child_invitation_repository( db: Annotated[Session, Depends(get_database)], -) -> SQLiteChildInvitationRepository: - """Get child invitation repository instance.""" - return SQLiteChildInvitationRepository(db) +) -> ChildInvitationRepositoryInterface: + """Get child invitation repository instance (works with any database).""" + return DatabaseChildInvitationRepository(db) diff --git a/src/baby_monitor/repositories/dependencies/get_child_repository.py b/src/baby_monitor/repositories/dependencies/get_child_repository.py index a5d677b..27cd71a 100644 --- a/src/baby_monitor/repositories/dependencies/get_child_repository.py +++ b/src/baby_monitor/repositories/dependencies/get_child_repository.py @@ -5,11 +5,14 @@ from fastapi import Depends from sqlalchemy.orm import Session from baby_monitor.repositories.dependencies.get_database import get_database -from baby_monitor.repositories.child.sqlite_child import SQLiteChildRepository +from baby_monitor.repositories.child.sqlite_child import DatabaseChildRepository +from baby_monitor.repositories.interfaces.child_repository_interface import ( + ChildRepositoryInterface, +) def get_child_repository( db: Annotated[Session, Depends(get_database)], -) -> SQLiteChildRepository: - """Get child repository instance.""" - return SQLiteChildRepository(db) +) -> ChildRepositoryInterface: + """Get child repository instance (works with any database).""" + return DatabaseChildRepository(db) diff --git a/src/baby_monitor/repositories/dependencies/get_database.py b/src/baby_monitor/repositories/dependencies/get_database.py index 229dd0e..e45d29b 100644 --- a/src/baby_monitor/repositories/dependencies/get_database.py +++ b/src/baby_monitor/repositories/dependencies/get_database.py @@ -10,18 +10,30 @@ from sqlalchemy.orm import declarative_base, sessionmaker, Session if TYPE_CHECKING: from sqlalchemy.orm import DeclarativeBase -# Database directory +# Database directory (used for SQLite) DATA_DIR = Path(os.getenv("DATA_DIR", "/data")) -# SQLite database URL -DATABASE_URL = f"sqlite:///{DATA_DIR}/baby_monitor.db" +# Check for PostgreSQL URI, otherwise use SQLite +POSTGRES_URI = os.getenv("POSTGRES_URI") -# Create engine with check_same_thread=False for SQLite -engine = create_engine( - DATABASE_URL, - connect_args={"check_same_thread": False}, - echo=False, # Set to True for SQL query logging -) +if POSTGRES_URI: + # Use PostgreSQL + DATABASE_URL = POSTGRES_URI + # PostgreSQL doesn't need special connect_args + engine = create_engine( + DATABASE_URL, + echo=False, # Set to True for SQL query logging + pool_pre_ping=True, # Verify connections before using them + ) +else: + # Use SQLite (default) + DATABASE_URL = f"sqlite:///{DATA_DIR}/baby_monitor.db" + # Create engine with check_same_thread=False for SQLite + engine = create_engine( + DATABASE_URL, + connect_args={"check_same_thread": False}, + echo=False, # Set to True for SQL query logging + ) # Session factory SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) @@ -33,7 +45,7 @@ Base: "type[DeclarativeBase]" = declarative_base() def _ensure_admin_user() -> None: """Create or update admin user from environment variables.""" from baby_monitor.models.db.user import User - from baby_monitor.utils.password import hash_password + from baby_monitor.utils import hash_password admin_username = os.getenv("ADMIN_USERNAME") admin_password = os.getenv("ADMIN_PASSWORD") @@ -78,15 +90,23 @@ def init_db() -> None: # This must be done before create_all() is called from baby_monitor.models.db.user import User # noqa: F401 from baby_monitor.models.db.child import Child # noqa: F401 - from baby_monitor.models.db.child_parent import ChildParent # noqa: F401 + from baby_monitor.models.db.child_parent import ( # noqa: F401 + ChildParent, + ) from baby_monitor.models.db.feeding import Feeding # noqa: F401 - from baby_monitor.models.db.diaper_change import DiaperChange # noqa: F401 + from baby_monitor.models.db.diaper_change import ( # noqa: F401 + DiaperChange, + ) from baby_monitor.models.db.sleep import Sleep # noqa: F401 from baby_monitor.models.invitation import Invitation # noqa: F401 - from baby_monitor.models.child_invitation import ChildInvitation # noqa: F401 + from baby_monitor.models.child_invitation import ( # noqa: F401 + ChildInvitation, + ) + + # Ensure data directory exists (only needed for SQLite) + if not POSTGRES_URI: + DATA_DIR.mkdir(parents=True, exist_ok=True) - # Ensure data directory exists - DATA_DIR.mkdir(parents=True, exist_ok=True) Base.metadata.create_all(bind=engine) # Create admin user if it doesn't exist diff --git a/src/baby_monitor/repositories/dependencies/get_diaper_change_repository.py b/src/baby_monitor/repositories/dependencies/get_diaper_change_repository.py index 5616377..3baf122 100644 --- a/src/baby_monitor/repositories/dependencies/get_diaper_change_repository.py +++ b/src/baby_monitor/repositories/dependencies/get_diaper_change_repository.py @@ -6,12 +6,15 @@ from sqlalchemy.orm import Session from baby_monitor.repositories.dependencies.get_database import get_database from baby_monitor.repositories.diaper_change.sqlite_diaper_change import ( - SQLiteDiaperChangeRepository, + DatabaseDiaperChangeRepository, +) +from baby_monitor.repositories.interfaces.diaper_change_repository_interface import ( # noqa: E501 + DiaperChangeRepositoryInterface, ) def get_diaper_change_repository( db: Annotated[Session, Depends(get_database)], -) -> SQLiteDiaperChangeRepository: - """Get diaper change repository instance.""" - return SQLiteDiaperChangeRepository(db) +) -> DiaperChangeRepositoryInterface: + """Get diaper change repository instance (works with any database).""" + return DatabaseDiaperChangeRepository(db) diff --git a/src/baby_monitor/repositories/dependencies/get_feeding_repository.py b/src/baby_monitor/repositories/dependencies/get_feeding_repository.py index 6f5cdb4..9a6c274 100644 --- a/src/baby_monitor/repositories/dependencies/get_feeding_repository.py +++ b/src/baby_monitor/repositories/dependencies/get_feeding_repository.py @@ -6,12 +6,15 @@ from sqlalchemy.orm import Session from baby_monitor.repositories.dependencies.get_database import get_database from baby_monitor.repositories.feeding.sqlite_feeding import ( - SQLiteFeedingRepository, + DatabaseFeedingRepository, +) +from baby_monitor.repositories.interfaces.feeding_repository_interface import ( + FeedingRepositoryInterface, ) def get_feeding_repository( db: Annotated[Session, Depends(get_database)], -) -> SQLiteFeedingRepository: - """Get feeding repository instance.""" - return SQLiteFeedingRepository(db) +) -> FeedingRepositoryInterface: + """Get feeding repository instance (works with any database).""" + return DatabaseFeedingRepository(db) diff --git a/src/baby_monitor/repositories/dependencies/get_invitation_repository.py b/src/baby_monitor/repositories/dependencies/get_invitation_repository.py index fce80f8..7528e6e 100644 --- a/src/baby_monitor/repositories/dependencies/get_invitation_repository.py +++ b/src/baby_monitor/repositories/dependencies/get_invitation_repository.py @@ -6,13 +6,16 @@ from sqlalchemy.orm import Session from baby_monitor.repositories.dependencies.get_database import get_database from baby_monitor.repositories.invitation.sqlite_invitation import ( - SQLiteInvitationRepository, + DatabaseInvitationRepository, +) +from baby_monitor.repositories.interfaces.invitation_repository_interface import ( + InvitationRepositoryInterface, ) def get_invitation_repository( db: Annotated[Session, Depends(get_database)], -) -> SQLiteInvitationRepository: +) -> InvitationRepositoryInterface: """ Provide an invitation repository instance. @@ -20,6 +23,6 @@ def get_invitation_repository( db: Database session from dependency injection Returns: - SQLiteInvitationRepository instance + InvitationRepositoryInterface instance (works with any database) """ - return SQLiteInvitationRepository(db) + return DatabaseInvitationRepository(db) diff --git a/src/baby_monitor/repositories/dependencies/get_sleep_repository.py b/src/baby_monitor/repositories/dependencies/get_sleep_repository.py index b3f5bea..133d89c 100644 --- a/src/baby_monitor/repositories/dependencies/get_sleep_repository.py +++ b/src/baby_monitor/repositories/dependencies/get_sleep_repository.py @@ -5,11 +5,14 @@ from fastapi import Depends from sqlalchemy.orm import Session from baby_monitor.repositories.dependencies.get_database import get_database -from baby_monitor.repositories.sleep.sqlite_sleep import SQLiteSleepRepository +from baby_monitor.repositories.sleep.sqlite_sleep import DatabaseSleepRepository +from baby_monitor.repositories.interfaces.sleep_repository_interface import ( + SleepRepositoryInterface, +) def get_sleep_repository( db: Annotated[Session, Depends(get_database)], -) -> SQLiteSleepRepository: - """Get sleep repository instance.""" - return SQLiteSleepRepository(db) +) -> SleepRepositoryInterface: + """Get sleep repository instance (works with any database).""" + return DatabaseSleepRepository(db) diff --git a/src/baby_monitor/repositories/dependencies/get_user_repository.py b/src/baby_monitor/repositories/dependencies/get_user_repository.py index 8d6fda4..c66685f 100644 --- a/src/baby_monitor/repositories/dependencies/get_user_repository.py +++ b/src/baby_monitor/repositories/dependencies/get_user_repository.py @@ -11,7 +11,7 @@ from baby_monitor.repositories.interfaces import ( UserRepositoryInterface, ) from baby_monitor.repositories.user.sqlite_user import ( - SQLiteUserRepository, + DatabaseUserRepository, ) # Singleton instance @@ -22,12 +22,12 @@ def get_user_repository( db: Annotated[Session, Depends(get_database)], ) -> UserRepositoryInterface: """ - Get user repository instance. + Get user repository instance (works with any database). """ global _user_repository if _user_repository is None: - _user_repository = SQLiteUserRepository(db) + _user_repository = DatabaseUserRepository(db) return _user_repository diff --git a/src/baby_monitor/repositories/diaper_change/sqlite_diaper_change.py b/src/baby_monitor/repositories/diaper_change/sqlite_diaper_change.py index f883c8e..18851fc 100644 --- a/src/baby_monitor/repositories/diaper_change/sqlite_diaper_change.py +++ b/src/baby_monitor/repositories/diaper_change/sqlite_diaper_change.py @@ -1,9 +1,12 @@ -"""SQLite implementation of diaper change repository.""" +"""Database-agnostic implementation of diaper change repository. + +Works with both SQLite and PostgreSQL. +""" from datetime import datetime from sqlalchemy.orm import Session -from baby_monitor.repositories.interfaces.diaper_change_repository_interface import ( # noqa: E501 +from baby_monitor.repositories.interfaces import ( # noqa: E501 DiaperChangeRepositoryInterface, ) from baby_monitor.models.db.diaper_change import DiaperChange @@ -11,8 +14,11 @@ from baby_monitor.models.db.child import Child from baby_monitor.models.db.child_parent import ChildParent -class SQLiteDiaperChangeRepository(DiaperChangeRepositoryInterface): - """SQLite implementation for diaper change log data access.""" +class DatabaseDiaperChangeRepository(DiaperChangeRepositoryInterface): + """Database implementation for diaper change log data access. + + Works with both SQLite and PostgreSQL databases. + """ def __init__(self, db: Session): self.db = db diff --git a/src/baby_monitor/repositories/feeding/sqlite_feeding.py b/src/baby_monitor/repositories/feeding/sqlite_feeding.py index e7a96a9..3d9226e 100644 --- a/src/baby_monitor/repositories/feeding/sqlite_feeding.py +++ b/src/baby_monitor/repositories/feeding/sqlite_feeding.py @@ -1,9 +1,12 @@ -"""SQLite implementation of feeding repository.""" +"""Database-agnostic implementation of feeding repository. + +Works with both SQLite and PostgreSQL. +""" from datetime import datetime from sqlalchemy.orm import Session -from baby_monitor.repositories.interfaces.feeding_repository_interface import ( +from baby_monitor.repositories.interfaces import ( FeedingRepositoryInterface, ) from baby_monitor.models.db.feeding import Feeding @@ -11,8 +14,11 @@ from baby_monitor.models.db.child import Child from baby_monitor.models.db.child_parent import ChildParent -class SQLiteFeedingRepository(FeedingRepositoryInterface): - """SQLite implementation for feeding log data access.""" +class DatabaseFeedingRepository(FeedingRepositoryInterface): + """Database implementation for feeding log data access. + + Works with both SQLite and PostgreSQL databases. + """ def __init__(self, db: Session): self.db = db diff --git a/src/baby_monitor/repositories/interfaces/__init__.py b/src/baby_monitor/repositories/interfaces/__init__.py index dd09ce8..72c9186 100644 --- a/src/baby_monitor/repositories/interfaces/__init__.py +++ b/src/baby_monitor/repositories/interfaces/__init__.py @@ -1,6 +1,6 @@ """Repository interfaces package.""" -from .child_invitation import ( +from .child_invitation_interface import ( ChildInvitationRepositoryInterface, ) from .credentials_repository_interface import ( diff --git a/src/baby_monitor/repositories/interfaces/child_invitation.py b/src/baby_monitor/repositories/interfaces/child_invitation_interface.py similarity index 100% rename from src/baby_monitor/repositories/interfaces/child_invitation.py rename to src/baby_monitor/repositories/interfaces/child_invitation_interface.py diff --git a/src/baby_monitor/repositories/invitation/__init__.py b/src/baby_monitor/repositories/invitation/__init__.py index 2ddabc3..b692596 100644 --- a/src/baby_monitor/repositories/invitation/__init__.py +++ b/src/baby_monitor/repositories/invitation/__init__.py @@ -1,7 +1,10 @@ """Invitation repository implementations.""" from baby_monitor.repositories.invitation.sqlite_invitation import ( - SQLiteInvitationRepository, + DatabaseInvitationRepository, ) -__all__ = ["SQLiteInvitationRepository"] +# Keep old name for backward compatibility +SQLiteInvitationRepository = DatabaseInvitationRepository + +__all__ = ["DatabaseInvitationRepository", "SQLiteInvitationRepository"] diff --git a/src/baby_monitor/repositories/invitation/sqlite_invitation.py b/src/baby_monitor/repositories/invitation/sqlite_invitation.py index e51882c..8140f23 100644 --- a/src/baby_monitor/repositories/invitation/sqlite_invitation.py +++ b/src/baby_monitor/repositories/invitation/sqlite_invitation.py @@ -1,25 +1,49 @@ -"""SQLite implementation of invitation repository. +"""Database-agnostic implementation of invitation repository. -Note: SQLite does not support timezone-aware datetimes natively. -All datetimes are stored as naive UTC and converted at the application layer. +Supports both SQLite and PostgreSQL: +- SQLite: stores datetimes as naive UTC +- PostgreSQL: stores datetimes as timezone-aware + +The repository automatically adapts by checking the timezone awareness +of datetimes returned from the database. """ from datetime import datetime, UTC from sqlalchemy.orm import Session from baby_monitor.models.invitation import Invitation -from baby_monitor.repositories.interfaces.invitation_repository_interface import ( +from baby_monitor.repositories.interfaces import ( InvitationRepositoryInterface, ) -class SQLiteInvitationRepository(InvitationRepositoryInterface): - """SQLite implementation for managing invitation tokens.""" +class DatabaseInvitationRepository(InvitationRepositoryInterface): + """Database implementation for managing invitation tokens. + + Works with both SQLite and PostgreSQL databases. + """ def __init__(self, db: Session) -> None: """Initialize the repository with a database session.""" self.db = db + def _get_now_utc(self, reference_dt: datetime) -> datetime: + """ + Get current UTC time matching the timezone awareness of reference. + + Args: + reference_dt: A datetime from DB to match timezone format + + Returns: + Current UTC time (naive for SQLite, aware for PostgreSQL) + """ + now = datetime.now(UTC) + if reference_dt.tzinfo is None: + # SQLite: return naive UTC + return now.replace(tzinfo=None) + # PostgreSQL: return timezone-aware + return now + def create_invitation( self, token: str, created_by_user_id: int, expires_at: datetime ) -> None: @@ -63,8 +87,8 @@ class SQLiteInvitationRepository(InvitationRepositoryInterface): if not invitation: return False - # Compare as naive UTC datetimes (SQLite stores without timezone) - now_utc = datetime.now(UTC).replace(tzinfo=None) + # Get current time matching database timezone format + now_utc = self._get_now_utc(invitation.expires_at) if invitation.expires_at < now_utc: return False @@ -89,8 +113,8 @@ class SQLiteInvitationRepository(InvitationRepositoryInterface): if not invitation: return False - # Compare as naive UTC datetimes (SQLite stores without timezone) - now_utc = datetime.now(UTC).replace(tzinfo=None) + # Get current time matching database timezone format + now_utc = self._get_now_utc(invitation.expires_at) if invitation.expires_at < now_utc: return False @@ -107,6 +131,14 @@ class SQLiteInvitationRepository(InvitationRepositoryInterface): def cleanup_expired(self) -> None: """Remove expired invitation tokens from storage.""" - now_utc = datetime.now(UTC).replace(tzinfo=None) + # For cleanup, we need to get a sample to determine format + # Get any invitation to check timezone format + sample = self.db.query(Invitation).first() + if sample: + now_utc = self._get_now_utc(sample.expires_at) + else: + # No invitations, use naive UTC (safe default) + now_utc = datetime.now(UTC).replace(tzinfo=None) + self.db.query(Invitation).filter(Invitation.expires_at < now_utc).delete() self.db.commit() diff --git a/src/baby_monitor/repositories/sleep/sqlite_sleep.py b/src/baby_monitor/repositories/sleep/sqlite_sleep.py index 0e641d0..1eb2bff 100644 --- a/src/baby_monitor/repositories/sleep/sqlite_sleep.py +++ b/src/baby_monitor/repositories/sleep/sqlite_sleep.py @@ -1,9 +1,12 @@ -"""SQLite implementation of sleep repository.""" +"""Database-agnostic implementation of sleep repository. + +Works with both SQLite and PostgreSQL. +""" from datetime import datetime from sqlalchemy.orm import Session -from baby_monitor.repositories.interfaces.sleep_repository_interface import ( +from baby_monitor.repositories.interfaces import ( SleepRepositoryInterface, ) from baby_monitor.models.db.sleep import Sleep @@ -11,8 +14,11 @@ from baby_monitor.models.db.child import Child from baby_monitor.models.db.child_parent import ChildParent -class SQLiteSleepRepository(SleepRepositoryInterface): - """SQLite implementation for sleep log data access.""" +class DatabaseSleepRepository(SleepRepositoryInterface): + """Database implementation for sleep log data access. + + Works with both SQLite and PostgreSQL databases. + """ def __init__(self, db: Session): self.db = db diff --git a/src/baby_monitor/repositories/user/postgresql_user.py b/src/baby_monitor/repositories/user/postgresql_user.py deleted file mode 100644 index 46491ff..0000000 --- a/src/baby_monitor/repositories/user/postgresql_user.py +++ /dev/null @@ -1,64 +0,0 @@ -"""PostgreSQL user repository (future implementation).""" - -from sqlalchemy.orm import Session - -from baby_monitor.repositories.interfaces import UserRepositoryInterface -from baby_monitor.models.db.user import User - - -class PostgreSQLUserRepository(UserRepositoryInterface): - """ - PostgreSQL-based user repository. - - This implementation uses the same SQLAlchemy models as SQLite, - just with a different database engine. - - To use this implementation: - 1. Add psycopg2-binary dependency - 2. Update DATABASE_URL in database.py: - DATABASE_URL = os.getenv( - "DATABASE_URL", - "postgresql://user:password@localhost:5432/baby_monitor" - ) - 3. Replace SQLiteUserRepository with this in dependencies - """ - - def __init__(self, db: Session): - self.db = db - - def get_by_username(self, username: str) -> dict | None: - """Get user by username.""" - user = self.db.query(User).filter(User.username == username).first() - if user: - return { - "id": user.id, - "username": user.username, - "hashed_password": user.hashed_password, - "created_at": user.created_at, - } - return None - - def create(self, username: str, hashed_password: str) -> dict: - """Create a new user.""" - user = User(username=username, hashed_password=hashed_password) - self.db.add(user) - self.db.commit() - self.db.refresh(user) - return { - "id": user.id, - "username": user.username, - "hashed_password": user.hashed_password, - "created_at": user.created_at, - } - - def get_by_id(self, user_id: int) -> dict | None: - """Get user by ID.""" - user = self.db.query(User).filter(User.id == user_id).first() - if user: - return { - "id": user.id, - "username": user.username, - "hashed_password": user.hashed_password, - "created_at": user.created_at, - } - return None diff --git a/src/baby_monitor/repositories/user/sqlite_user.py b/src/baby_monitor/repositories/user/sqlite_user.py index 2cf6ac4..5cd8ad7 100644 --- a/src/baby_monitor/repositories/user/sqlite_user.py +++ b/src/baby_monitor/repositories/user/sqlite_user.py @@ -1,4 +1,7 @@ -"""SQLite implementation of user repository.""" +"""Database-agnostic implementation of user repository. + +Works with both SQLite and PostgreSQL. +""" from sqlalchemy.orm import Session @@ -6,8 +9,11 @@ from baby_monitor.repositories.interfaces import UserRepositoryInterface from baby_monitor.models.db.user import User -class SQLiteUserRepository(UserRepositoryInterface): - """SQLite-based user repository.""" +class DatabaseUserRepository(UserRepositoryInterface): + """Database-based user repository. + + Works with both SQLite and PostgreSQL databases. + """ def __init__(self, db: Session): self.db = db diff --git a/src/baby_monitor/routers/admin.py b/src/baby_monitor/routers/admin.py index 49d697b..31847c9 100644 --- a/src/baby_monitor/routers/admin.py +++ b/src/baby_monitor/routers/admin.py @@ -7,22 +7,12 @@ from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from baby_monitor.routers.auth import verify_admin -from baby_monitor.repositories.dependencies.get_invitation_repository import ( +from baby_monitor.repositories.dependencies import ( get_invitation_repository, -) -from baby_monitor.repositories.dependencies.get_user_repository import ( get_user_repository, -) -from baby_monitor.repositories.dependencies.get_child_repository import ( get_child_repository, -) -from baby_monitor.repositories.dependencies.get_feeding_repository import ( get_feeding_repository, -) -from baby_monitor.repositories.dependencies.get_diaper_change_repository import ( get_diaper_change_repository, -) -from baby_monitor.repositories.dependencies.get_sleep_repository import ( get_sleep_repository, ) from baby_monitor.repositories.interfaces import ( diff --git a/src/baby_monitor/routers/auth.py b/src/baby_monitor/routers/auth.py index 294d0c5..33da6ac 100644 --- a/src/baby_monitor/routers/auth.py +++ b/src/baby_monitor/routers/auth.py @@ -1,8 +1,8 @@ """Authentication router for login and user management.""" import secrets -from fastapi import APIRouter, HTTPException, Depends, Header from typing import Annotated +from fastapi import APIRouter, HTTPException, Depends, Header from pydantic import BaseModel from baby_monitor.models.auth import ( @@ -29,7 +29,7 @@ from baby_monitor.repositories.interfaces import ( DiaperChangeRepositoryInterface, SleepRepositoryInterface, ) -from baby_monitor.utils.password import hash_password, verify_password +from baby_monitor.utils import hash_password, verify_password router = APIRouter(prefix="/api", tags=["authentication"]) diff --git a/src/baby_monitor/routers/child.py b/src/baby_monitor/routers/child.py index ead85c6..456e78b 100644 --- a/src/baby_monitor/routers/child.py +++ b/src/baby_monitor/routers/child.py @@ -8,14 +8,14 @@ from pydantic import BaseModel from baby_monitor.models.child import CreateChildRequest, ChildResponse from baby_monitor.routers.auth import verify_token -from baby_monitor.repositories.child.sqlite_child import SQLiteChildRepository -from baby_monitor.repositories.dependencies.get_child_repository import ( - get_child_repository, +from baby_monitor.repositories.interfaces import ( + ChildRepositoryInterface, + ChildInvitationRepositoryInterface, ) -from baby_monitor.repositories.dependencies.get_child_invitation_repository import ( # noqa: E501 +from baby_monitor.repositories.dependencies import ( + get_child_repository, get_child_invitation_repository, ) -from baby_monitor.repositories.interfaces import ChildInvitationRepositoryInterface # noqa: E501 router = APIRouter(prefix="/api/children", tags=["children"]) @@ -45,7 +45,7 @@ class RedeemChildInvitationRequest(BaseModel): def create_child( request: CreateChildRequest, user_id: Annotated[int, Depends(verify_token)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> ChildResponse: """Create a new child for the authenticated user.""" child = child_repo.create( @@ -61,7 +61,7 @@ def create_child( @router.get("", response_model=list[ChildResponse]) def get_user_children( user_id: Annotated[int, Depends(verify_token)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> list[ChildResponse]: """Get all children for the authenticated user.""" children = child_repo.get_by_user_id(user_id) @@ -72,7 +72,7 @@ def get_user_children( def get_child( child_id: int, user_id: Annotated[int, Depends(verify_token)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> ChildResponse: """Get a specific child by ID.""" child = child_repo.get_by_id(child_id) @@ -93,7 +93,7 @@ def update_child( child_id: int, request: CreateChildRequest, user_id: Annotated[int, Depends(verify_token)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> ChildResponse: """Update a child's information.""" # First check if child exists and belongs to user @@ -124,10 +124,11 @@ def update_child( def create_child_invitation( request: CreateChildInvitationRequest, user_id: Annotated[int, Depends(verify_token)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], # noqa: E501 + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], invitation_repo: Annotated[ - ChildInvitationRepositoryInterface, Depends(get_child_invitation_repository) - ], # noqa: E501 + ChildInvitationRepositoryInterface, + Depends(get_child_invitation_repository), + ], ) -> ChildInvitationResponse: """Create an invitation code to share child access with another user.""" # Verify the child exists and user has access @@ -165,10 +166,11 @@ def create_child_invitation( def redeem_child_invitation( request: RedeemChildInvitationRequest, user_id: Annotated[int, Depends(verify_token)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], # noqa: E501 + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], invitation_repo: Annotated[ - ChildInvitationRepositoryInterface, Depends(get_child_invitation_repository) - ], # noqa: E501 + ChildInvitationRepositoryInterface, + Depends(get_child_invitation_repository), + ], ) -> dict: """Redeem an invitation code to gain access to a child.""" # Verify the invitation code diff --git a/src/baby_monitor/routers/diaper_change.py b/src/baby_monitor/routers/diaper_change.py index a62f063..312847d 100644 --- a/src/baby_monitor/routers/diaper_change.py +++ b/src/baby_monitor/routers/diaper_change.py @@ -9,17 +9,15 @@ from baby_monitor.models.diaper_change import ( DiaperChangeResponse, ) from baby_monitor.routers.auth import verify_token -from baby_monitor.repositories.diaper_change.sqlite_diaper_change import ( - SQLiteDiaperChangeRepository, +from baby_monitor.repositories.interfaces import ( + DiaperChangeRepositoryInterface, + ChildRepositoryInterface, ) -from baby_monitor.repositories.dependencies.get_diaper_change_repository import ( # noqa: E501 +from baby_monitor.repositories.dependencies import ( get_diaper_change_repository, -) -from baby_monitor.repositories.child.sqlite_child import SQLiteChildRepository -from baby_monitor.repositories.dependencies.get_child_repository import ( get_child_repository, ) -from baby_monitor.utils.access_control import verify_child_access +from baby_monitor.utils.verify_child_access import verify_child_access router = APIRouter(prefix="/api/diaper-changes", tags=["diaper-changes"]) @@ -29,9 +27,9 @@ def create_diaper_change( request: CreateDiaperChangeRequest, user_id: Annotated[int, Depends(verify_token)], diaper_repo: Annotated[ - SQLiteDiaperChangeRepository, Depends(get_diaper_change_repository) + DiaperChangeRepositoryInterface, Depends(get_diaper_change_repository) ], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> DiaperChangeResponse: """Create a new diaper change log entry.""" # Verify the child belongs to the authenticated user @@ -53,7 +51,7 @@ def create_diaper_change( def get_user_diaper_changes( user_id: Annotated[int, Depends(verify_token)], diaper_repo: Annotated[ - SQLiteDiaperChangeRepository, Depends(get_diaper_change_repository) + DiaperChangeRepositoryInterface, Depends(get_diaper_change_repository) ], ) -> list[DiaperChangeResponse]: """Get all diaper change logs for the authenticated user's children.""" @@ -66,9 +64,9 @@ def get_diaper_change( diaper_change_id: int, user_id: Annotated[int, Depends(verify_token)], diaper_repo: Annotated[ - SQLiteDiaperChangeRepository, Depends(get_diaper_change_repository) + DiaperChangeRepositoryInterface, Depends(get_diaper_change_repository) ], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> DiaperChangeResponse: """Get a specific diaper change log by ID.""" diaper_change = diaper_repo.get_by_id(diaper_change_id) @@ -88,9 +86,9 @@ def update_diaper_change( request: UpdateDiaperChangeRequest, user_id: Annotated[int, Depends(verify_token)], diaper_repo: Annotated[ - SQLiteDiaperChangeRepository, Depends(get_diaper_change_repository) + DiaperChangeRepositoryInterface, Depends(get_diaper_change_repository) ], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> DiaperChangeResponse: """Update an existing diaper change log.""" diaper_change = diaper_repo.get_by_id(diaper_change_id) @@ -123,9 +121,9 @@ def delete_diaper_change( diaper_change_id: int, user_id: Annotated[int, Depends(verify_token)], diaper_repo: Annotated[ - SQLiteDiaperChangeRepository, Depends(get_diaper_change_repository) + DiaperChangeRepositoryInterface, Depends(get_diaper_change_repository) ], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> None: """Delete a diaper change log.""" diaper_change = diaper_repo.get_by_id(diaper_change_id) diff --git a/src/baby_monitor/routers/feeding.py b/src/baby_monitor/routers/feeding.py index c205738..929d038 100644 --- a/src/baby_monitor/routers/feeding.py +++ b/src/baby_monitor/routers/feeding.py @@ -9,17 +9,15 @@ from baby_monitor.models.feeding import ( FeedingResponse, ) from baby_monitor.routers.auth import verify_token -from baby_monitor.repositories.feeding.sqlite_feeding import ( - SQLiteFeedingRepository, +from baby_monitor.repositories.interfaces import ( + ChildRepositoryInterface, + FeedingRepositoryInterface, ) -from baby_monitor.repositories.dependencies.get_feeding_repository import ( +from baby_monitor.repositories.dependencies import ( get_feeding_repository, -) -from baby_monitor.repositories.child.sqlite_child import SQLiteChildRepository -from baby_monitor.repositories.dependencies.get_child_repository import ( get_child_repository, ) -from baby_monitor.utils.access_control import verify_child_access +from baby_monitor.utils import verify_child_access router = APIRouter(prefix="/api/feedings", tags=["feedings"]) @@ -28,8 +26,10 @@ router = APIRouter(prefix="/api/feedings", tags=["feedings"]) def create_feeding( request: CreateFeedingRequest, user_id: Annotated[int, Depends(verify_token)], - feeding_repo: Annotated[SQLiteFeedingRepository, Depends(get_feeding_repository)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + feeding_repo: Annotated[ + FeedingRepositoryInterface, Depends(get_feeding_repository) + ], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> FeedingResponse: """Create a new feeding log entry.""" # Verify the child belongs to the authenticated user @@ -48,8 +48,10 @@ def create_feeding( @router.get("/active", response_model=FeedingResponse | None) def get_active_feeding( user_id: Annotated[int, Depends(verify_token)], - feeding_repo: Annotated[SQLiteFeedingRepository, Depends(get_feeding_repository)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + feeding_repo: Annotated[ + FeedingRepositoryInterface, Depends(get_feeding_repository) + ], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> FeedingResponse | None: """Get the current active feeding (where end_time is null) for the user.""" feedings = feeding_repo.get_by_user_id(user_id) @@ -70,7 +72,9 @@ def get_active_feeding( @router.get("", response_model=list[FeedingResponse]) def get_user_feedings( user_id: Annotated[int, Depends(verify_token)], - feeding_repo: Annotated[SQLiteFeedingRepository, Depends(get_feeding_repository)], + feeding_repo: Annotated[ + FeedingRepositoryInterface, Depends(get_feeding_repository) + ], ) -> list[FeedingResponse]: """Get all feeding logs for the authenticated user's children.""" feedings = feeding_repo.get_by_user_id(user_id) @@ -81,8 +85,10 @@ def get_user_feedings( def get_feeding( feeding_id: int, user_id: Annotated[int, Depends(verify_token)], - feeding_repo: Annotated[SQLiteFeedingRepository, Depends(get_feeding_repository)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + feeding_repo: Annotated[ + FeedingRepositoryInterface, Depends(get_feeding_repository) + ], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> FeedingResponse: """Get a specific feeding log by ID.""" feeding = feeding_repo.get_by_id(feeding_id) @@ -101,8 +107,10 @@ def update_feeding( feeding_id: int, request: UpdateFeedingRequest, user_id: Annotated[int, Depends(verify_token)], - feeding_repo: Annotated[SQLiteFeedingRepository, Depends(get_feeding_repository)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + feeding_repo: Annotated[ + FeedingRepositoryInterface, Depends(get_feeding_repository) + ], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> FeedingResponse: """Update a feeding log entry.""" feeding = feeding_repo.get_by_id(feeding_id) @@ -114,11 +122,12 @@ def update_feeding( verify_child_access(child_repo, feeding["child_id"], user_id) # Update the feeding + feeding_type_value = request.feeding_type.value if request.feeding_type else None updated_feeding = feeding_repo.update( feeding_id=feeding_id, start_time=request.start_time, end_time=request.end_time, - feeding_type=request.feeding_type.value if request.feeding_type else None, + feeding_type=feeding_type_value, ) if not updated_feeding: @@ -131,8 +140,10 @@ def update_feeding( def delete_feeding( feeding_id: int, user_id: Annotated[int, Depends(verify_token)], - feeding_repo: Annotated[SQLiteFeedingRepository, Depends(get_feeding_repository)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + feeding_repo: Annotated[ + FeedingRepositoryInterface, Depends(get_feeding_repository) + ], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> None: """Delete a feeding log entry.""" feeding = feeding_repo.get_by_id(feeding_id) diff --git a/src/baby_monitor/routers/sleep.py b/src/baby_monitor/routers/sleep.py index a2308b2..a04d10f 100644 --- a/src/baby_monitor/routers/sleep.py +++ b/src/baby_monitor/routers/sleep.py @@ -9,15 +9,15 @@ from baby_monitor.models.sleep import ( SleepResponse, ) from baby_monitor.routers.auth import verify_token -from baby_monitor.repositories.sleep.sqlite_sleep import SQLiteSleepRepository -from baby_monitor.repositories.dependencies.get_sleep_repository import ( - get_sleep_repository, +from baby_monitor.repositories.interfaces import ( + SleepRepositoryInterface, + ChildRepositoryInterface, ) -from baby_monitor.repositories.child.sqlite_child import SQLiteChildRepository -from baby_monitor.repositories.dependencies.get_child_repository import ( +from baby_monitor.repositories.dependencies import ( + get_sleep_repository, get_child_repository, ) -from baby_monitor.utils.access_control import verify_child_access +from baby_monitor.utils import verify_child_access router = APIRouter(prefix="/api/sleep", tags=["sleep"]) @@ -26,8 +26,8 @@ router = APIRouter(prefix="/api/sleep", tags=["sleep"]) def create_sleep( request: CreateSleepRequest, user_id: Annotated[int, Depends(verify_token)], - sleep_repo: Annotated[SQLiteSleepRepository, Depends(get_sleep_repository)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + sleep_repo: Annotated[SleepRepositoryInterface, Depends(get_sleep_repository)], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> SleepResponse: """Create a new sleep log entry.""" # Verify the child belongs to the authenticated user @@ -44,8 +44,8 @@ def create_sleep( @router.get("/active", response_model=SleepResponse | None) def get_active_sleep( user_id: Annotated[int, Depends(verify_token)], - sleep_repo: Annotated[SQLiteSleepRepository, Depends(get_sleep_repository)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + sleep_repo: Annotated[SleepRepositoryInterface, Depends(get_sleep_repository)], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> SleepResponse | None: """Get the current active sleep (where end_time is null) for the user.""" sleeps = sleep_repo.get_by_user_id(user_id) @@ -66,7 +66,7 @@ def get_active_sleep( @router.get("", response_model=list[SleepResponse]) def get_user_sleeps( user_id: Annotated[int, Depends(verify_token)], - sleep_repo: Annotated[SQLiteSleepRepository, Depends(get_sleep_repository)], + sleep_repo: Annotated[SleepRepositoryInterface, Depends(get_sleep_repository)], ) -> list[SleepResponse]: """Get all sleep logs for the authenticated user's children.""" sleeps = sleep_repo.get_by_user_id(user_id) @@ -77,8 +77,8 @@ def get_user_sleeps( def get_sleep( sleep_id: int, user_id: Annotated[int, Depends(verify_token)], - sleep_repo: Annotated[SQLiteSleepRepository, Depends(get_sleep_repository)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + sleep_repo: Annotated[SleepRepositoryInterface, Depends(get_sleep_repository)], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> SleepResponse: """Get a specific sleep log by ID.""" sleep = sleep_repo.get_by_id(sleep_id) @@ -97,8 +97,8 @@ def update_sleep( sleep_id: int, request: UpdateSleepRequest, user_id: Annotated[int, Depends(verify_token)], - sleep_repo: Annotated[SQLiteSleepRepository, Depends(get_sleep_repository)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + sleep_repo: Annotated[SleepRepositoryInterface, Depends(get_sleep_repository)], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> SleepResponse: """Update an existing sleep log.""" sleep = sleep_repo.get_by_id(sleep_id) @@ -124,8 +124,8 @@ def update_sleep( def delete_sleep( sleep_id: int, user_id: Annotated[int, Depends(verify_token)], - sleep_repo: Annotated[SQLiteSleepRepository, Depends(get_sleep_repository)], - child_repo: Annotated[SQLiteChildRepository, Depends(get_child_repository)], + sleep_repo: Annotated[SleepRepositoryInterface, Depends(get_sleep_repository)], + child_repo: Annotated[ChildRepositoryInterface, Depends(get_child_repository)], ) -> None: """Delete a sleep log.""" sleep = sleep_repo.get_by_id(sleep_id) diff --git a/src/baby_monitor/static/add-child.html b/src/baby_monitor/static/add-child.html index 2bbcfa6..85e7336 100644 --- a/src/baby_monitor/static/add-child.html +++ b/src/baby_monitor/static/add-child.html @@ -192,12 +192,20 @@ Cancel - -