added postgres support
This commit is contained in:
+3
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}')>"
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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})>"
|
||||
|
||||
@@ -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})>"
|
||||
|
||||
@@ -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})>"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user