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/
# 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
+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
| 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 |
| `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
+1 -1
View File
@@ -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:
+4 -2
View File
@@ -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"<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)
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 (
+4 -2
View File
@@ -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"<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)
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"<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)
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"<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)
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"<User(id={self.id}, username='{self.username}')>"
@@ -10,12 +10,24 @@ 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")
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,
@@ -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
# Ensure data directory exists (only needed for SQLite)
if not POSTGRES_URI:
DATA_DIR.mkdir(parents=True, exist_ok=True)
Base.metadata.create_all(bind=engine)
# Create admin user if it doesn't exist