added postgres support
This commit is contained in:
+3
-1
@@ -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
|
||||||
|
|||||||
@@ -81,13 +81,14 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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}')>"
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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})>"
|
||||||
|
|||||||
@@ -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})>"
|
||||||
|
|||||||
@@ -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})>"
|
||||||
|
|||||||
@@ -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,
|
DATABASE_URL = POSTGRES_URI
|
||||||
connect_args={"check_same_thread": False},
|
# PostgreSQL doesn't need special connect_args
|
||||||
echo=False, # Set to True for SQL query logging
|
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
|
# 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 (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)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
# Create admin user if it doesn't exist
|
# Create admin user if it doesn't exist
|
||||||
|
|||||||
Reference in New Issue
Block a user