added postgres support

This commit is contained in:
Brian Bjarke Jensen
2025-11-11 15:16:24 +01:00
parent 7c602c8da7
commit 65655db034
10 changed files with 83 additions and 40 deletions
+3 -1
View File
@@ -23,7 +23,9 @@ COPY pyproject.toml README.md ./
COPY src/ ./src/ COPY src/ ./src/
# Install Python dependencies including all optional dependencies # 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 RUN uv sync --no-dev --all-extras
# Add virtual environment to PATH # Add virtual environment to PATH
+16 -7
View File
@@ -82,12 +82,13 @@ uv run uvicorn src.baby_monitor.main:app --reload --host 0.0.0.0 --port 8000
### Environment Variables ### Environment Variables
| Variable | Default | Description | | Variable | Default | Description |
| ---------------- | ------------ | ------------------------------------------- | | ---------------- | ------------ | --------------------------------------------- |
| `ENVIRONMENT` | `production` | Set to `development` to enable API docs | | `ENVIRONMENT` | `production` | Set to `development` to enable API docs |
| `ADMIN_PASSWORD` | _(required)_ | Admin user password | | `ADMIN_PASSWORD` | _(required)_ | Admin user password |
| `ADMIN_USERNAME` | `admin` | Admin username | | `ADMIN_USERNAME` | `admin` | Admin username |
| `DATA_DIR` | `/data` | Directory for SQLite database | | `DATA_DIR` | `/data` | Directory for SQLite database (SQLite only) |
| `REDIS_URI` | _(optional)_ | Redis connection URI for distributed tokens | | `REDIS_URI` | _(optional)_ | Redis connection URI for distributed tokens |
| `POSTGRES_URI` | _(optional)_ | PostgreSQL connection URI for database storage|
### Storage Options ### 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 - Automatic setup, no configuration needed
- Data stored in `/data/baby_monitor.db` - Data stored in `/data/baby_monitor.db`
- Perfect for single-server deployments - Perfect for single-server deployments
- Timezone-aware datetimes stored as naive UTC
**Redis (Optional)** **Redis (Optional - Token Storage)**
```bash ```bash
# Enable Redis token storage # Enable Redis token storage for distributed deployments
export REDIS_URI=redis://localhost:6379 export REDIS_URI=redis://localhost:6379
``` ```
**PostgreSQL (Future)** **PostgreSQL (Optional - Database)**
- Repository interface ready ```bash
- Swap implementation in `dependencies.py` # 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 ## 📁 Project Structure
+1 -1
View File
@@ -17,7 +17,7 @@ services:
# Use Redis for token storage # Use Redis for token storage
- REDIS_URI=redis://redis:6379/0 - REDIS_URI=redis://redis:6379/0
# Use PostgreSQL for database # 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"] command: ["--host", "0.0.0.0", "--port", "8000"]
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
+4 -2
View File
@@ -19,9 +19,11 @@ class Child(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False) 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 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: def __repr__(self) -> str:
return f"<Child(id={self.id}, name='{self.name}')>" return f"<Child(id={self.id}, name='{self.name}')>"
+3 -1
View File
@@ -20,7 +20,9 @@ class ChildParent(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
child_id = Column(Integer, ForeignKey("children.id"), nullable=False) child_id = Column(Integer, ForeignKey("children.id"), nullable=False)
user_id = Column(Integer, ForeignKey("users.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: def __repr__(self) -> str:
return ( return (
+4 -2
View File
@@ -19,12 +19,14 @@ class DiaperChange(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
child_id = Column(Integer, ForeignKey("children.id"), nullable=False) 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_amount = Column(String, nullable=True) # stores PoopAmount enum
poop_color = Column(String, nullable=True) # stores PoopColor enum poop_color = Column(String, nullable=True) # stores PoopColor enum
pee_amount = Column(String, nullable=True) # stores PeeAmount enum pee_amount = Column(String, nullable=True) # stores PeeAmount enum
pee_color = Column(String, nullable=True) # stores PeeColor 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: def __repr__(self) -> str:
return f"<DiaperChange(id={self.id}, child_id={self.child_id})>" return f"<DiaperChange(id={self.id}, child_id={self.child_id})>"
+5 -3
View File
@@ -19,10 +19,12 @@ class Feeding(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
child_id = Column(Integer, ForeignKey("children.id"), nullable=False) child_id = Column(Integer, ForeignKey("children.id"), nullable=False)
start_time = Column(DateTime, nullable=False) start_time = Column(DateTime(timezone=True), nullable=False)
end_time = Column(DateTime, nullable=True) end_time = Column(DateTime(timezone=True), nullable=True)
feeding_type = Column(String, nullable=False) # stores FeedingType enum 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: def __repr__(self) -> str:
return f"<Feeding(id={self.id}, child_id={self.child_id})>" return f"<Feeding(id={self.id}, child_id={self.child_id})>"
+5 -3
View File
@@ -19,9 +19,11 @@ class Sleep(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
child_id = Column(Integer, ForeignKey("children.id"), nullable=False) child_id = Column(Integer, ForeignKey("children.id"), nullable=False)
start_time = Column(DateTime, nullable=False) start_time = Column(DateTime(timezone=True), nullable=False)
end_time = Column(DateTime, nullable=True) end_time = Column(DateTime(timezone=True), nullable=True)
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: def __repr__(self) -> str:
return f"<Sleep(id={self.id}, child_id={self.child_id})>" return f"<Sleep(id={self.id}, child_id={self.child_id})>"
+3 -1
View File
@@ -21,7 +21,9 @@ class User(Base):
username = Column(String, unique=True, index=True, nullable=False) username = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False) hashed_password = Column(String, nullable=False)
is_admin = Column(Boolean, default=False, 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: def __repr__(self) -> str:
return f"<User(id={self.id}, username='{self.username}')>" return f"<User(id={self.id}, username='{self.username}')>"
@@ -10,18 +10,30 @@ from sqlalchemy.orm import declarative_base, sessionmaker, Session
if TYPE_CHECKING: if TYPE_CHECKING:
from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DeclarativeBase
# Database directory # Database directory (used for SQLite)
DATA_DIR = Path(os.getenv("DATA_DIR", "/data")) DATA_DIR = Path(os.getenv("DATA_DIR", "/data"))
# SQLite database URL # Check for PostgreSQL URI, otherwise use SQLite
DATABASE_URL = f"sqlite:///{DATA_DIR}/baby_monitor.db" POSTGRES_URI = os.getenv("POSTGRES_URI")
# Create engine with check_same_thread=False for SQLite if POSTGRES_URI:
engine = create_engine( # 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, DATABASE_URL,
connect_args={"check_same_thread": False}, connect_args={"check_same_thread": False},
echo=False, # Set to True for SQL query logging echo=False, # Set to True for SQL query logging
) )
# Session factory # Session factory
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 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 # This must be done before create_all() is called
from baby_monitor.models.db.user import User # noqa: F401 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 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.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.db.sleep import Sleep # noqa: F401
from baby_monitor.models.invitation import Invitation # 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 # Ensure data directory exists (only needed for SQLite)
if not POSTGRES_URI:
DATA_DIR.mkdir(parents=True, exist_ok=True) DATA_DIR.mkdir(parents=True, exist_ok=True)
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
# Create admin user if it doesn't exist # Create admin user if it doesn't exist