From 65655db0343c288140dce77f21ef2d2ad4df193c Mon Sep 17 00:00:00 2001 From: Brian Bjarke Jensen Date: Tue, 11 Nov 2025 15:16:24 +0100 Subject: [PATCH] added postgres support --- Dockerfile | 4 +- README.md | 33 ++++++++----- docker-compose.prod.yml | 2 +- src/baby_monitor/models/db/child.py | 6 ++- src/baby_monitor/models/db/child_parent.py | 4 +- src/baby_monitor/models/db/diaper_change.py | 6 ++- src/baby_monitor/models/db/feeding.py | 8 ++-- src/baby_monitor/models/db/sleep.py | 8 ++-- src/baby_monitor/models/db/user.py | 4 +- .../repositories/dependencies/get_database.py | 48 +++++++++++++------ 10 files changed, 83 insertions(+), 40 deletions(-) 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..650b6c7 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..f636a7b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -17,7 +17,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..96a9bda 100644 --- a/src/baby_monitor/models/db/sleep.py +++ b/src/baby_monitor/models/db/sleep.py @@ -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/dependencies/get_database.py b/src/baby_monitor/repositories/dependencies/get_database.py index 229dd0e..414486a 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) @@ -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