Compare commits
13 Commits
918fc91604
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e20e5eac01 | ||
|
|
e9803eff37 | ||
|
|
41123f046e | ||
|
|
013c398296 | ||
|
|
6c4acf438d | ||
|
|
bef2577e13 | ||
|
|
ed896a2bdf | ||
|
|
7deeb681c0 | ||
|
|
8814dd8994 | ||
|
|
02247340d4 | ||
|
|
f3c14d649c | ||
|
|
b71187009b | ||
|
|
77cea8cbc5 |
@@ -1,46 +1,44 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
venv/
|
||||
|
||||
# Database files (will be mounted as volume)
|
||||
*.db
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Database
|
||||
*.db
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Uploads
|
||||
static/uploads/*
|
||||
!static/uploads/.gitkeep
|
||||
# Development files
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
# Cache directories
|
||||
.cache/
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -8,3 +8,16 @@ wheels/
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
pet_pictures.db
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
201
CLAUDE.md
Normal file
201
CLAUDE.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# Claude Development Guidelines
|
||||
|
||||
## Database Migrations
|
||||
|
||||
**⚠️ IMPORTANT: When making changes to database models or schema, you MUST create migration files.**
|
||||
|
||||
### When to Create Migrations
|
||||
|
||||
**ALWAYS create a migration when:**
|
||||
- Adding new columns to existing tables
|
||||
- Creating new tables
|
||||
- Modifying column types or constraints
|
||||
- Adding or removing indexes
|
||||
- Changing default values
|
||||
- Adding foreign keys or relationships
|
||||
|
||||
**Examples that require migrations:**
|
||||
```python
|
||||
# Adding a new field to PetPicture model
|
||||
class PetPicture:
|
||||
# ... existing fields ...
|
||||
tags = models.TextField() # ❌ Requires migration!
|
||||
```
|
||||
|
||||
```python
|
||||
# Changing database schema in init_db()
|
||||
def init_db():
|
||||
db.execute("""
|
||||
CREATE TABLE users ( -- ❌ New table requires migration!
|
||||
id INTEGER PRIMARY KEY,
|
||||
username TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
```
|
||||
|
||||
### How to Create Migrations
|
||||
|
||||
1. **Navigate to migrations directory:**
|
||||
```bash
|
||||
cd migrations/
|
||||
```
|
||||
|
||||
2. **Create new migration file:**
|
||||
```bash
|
||||
# Use next sequential number (002, 003, etc.)
|
||||
touch XXX_descriptive_name.py
|
||||
```
|
||||
|
||||
3. **Use the migration template:**
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration XXX: Brief description of changes
|
||||
Date: YYYY-MM-DD
|
||||
Description: Detailed explanation of what this migration does
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
def check_column_exists(cursor, table_name, column_name):
|
||||
"""Check if a column exists in a table"""
|
||||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
return column_name in columns
|
||||
|
||||
def migrate_up(cursor):
|
||||
"""Apply the migration"""
|
||||
# Check if already applied
|
||||
if check_column_exists(cursor, 'pet_pictures', 'new_column'):
|
||||
print("Migration already applied, skipping...")
|
||||
return False
|
||||
|
||||
# Apply the migration
|
||||
print("Adding new_column to pet_pictures table...")
|
||||
cursor.execute("ALTER TABLE pet_pictures ADD COLUMN new_column TEXT")
|
||||
return True
|
||||
|
||||
def migrate_down(cursor):
|
||||
"""Rollback the migration (optional)"""
|
||||
print("Rollback not implemented for SQLite column drops")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Run migration standalone"""
|
||||
# Standard migration execution code
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
```
|
||||
|
||||
4. **Test the migration:**
|
||||
```bash
|
||||
# Test individual migration
|
||||
python migrations/XXX_descriptive_name.py
|
||||
|
||||
# Or run all pending migrations
|
||||
python migrations/migrate.py
|
||||
```
|
||||
|
||||
5. **Commit the migration file:**
|
||||
```bash
|
||||
git add migrations/XXX_descriptive_name.py
|
||||
git commit -m "feat: add migration for [description]"
|
||||
```
|
||||
|
||||
### Migration Naming Convention
|
||||
|
||||
- **Sequential numbering**: `001_`, `002_`, `003_`, etc.
|
||||
- **Descriptive names**: Explain what the migration does
|
||||
- **Examples**:
|
||||
- `002_add_user_table.py`
|
||||
- `003_add_tags_column_to_pictures.py`
|
||||
- `004_create_comments_table.py`
|
||||
|
||||
### DO NOT Modify Database Schema Without Migrations
|
||||
|
||||
**❌ NEVER do this:**
|
||||
```python
|
||||
# Directly modifying init_db() or model schema
|
||||
def init_db():
|
||||
db.execute("""
|
||||
CREATE TABLE pet_pictures (
|
||||
id INTEGER PRIMARY KEY,
|
||||
filename TEXT NOT NULL,
|
||||
new_field TEXT -- ❌ Added without migration!
|
||||
)
|
||||
""")
|
||||
```
|
||||
|
||||
**✅ ALWAYS do this:**
|
||||
1. Create migration file first
|
||||
2. Run migration to update schema
|
||||
3. Then update model code if needed
|
||||
|
||||
### Model Changes Workflow
|
||||
|
||||
1. **Identify needed schema change**
|
||||
2. **Create migration file**
|
||||
3. **Test migration locally**
|
||||
4. **Update model/application code** (if needed)
|
||||
5. **Test full application**
|
||||
6. **Commit migration + code changes together**
|
||||
|
||||
### Migration Best Practices
|
||||
|
||||
- **One logical change per migration**
|
||||
- **Make migrations idempotent** (safe to run multiple times)
|
||||
- **Test on development data first**
|
||||
- **Include rollback logic when possible**
|
||||
- **Document breaking changes clearly**
|
||||
- **Never edit applied migrations**
|
||||
|
||||
### Integration Points
|
||||
|
||||
**Development:**
|
||||
```bash
|
||||
# Apply migrations before starting app
|
||||
python migrations/migrate.py && python main.py
|
||||
```
|
||||
|
||||
**Docker:**
|
||||
```bash
|
||||
# Migrations run automatically via docker-compose
|
||||
docker compose --profile migrate up migrate
|
||||
```
|
||||
|
||||
**Production Deployment:**
|
||||
```bash
|
||||
# Run migrations as part of deployment
|
||||
python migrations/migrate.py
|
||||
systemctl restart pet-picture-queue
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema Reference
|
||||
|
||||
**Current Schema (after migrations):**
|
||||
|
||||
### `pet_pictures` table:
|
||||
- `id` - INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
- `filename` - TEXT NOT NULL
|
||||
- `subscriber_name` - TEXT NOT NULL
|
||||
- `description` - TEXT NULL (added via migration 001)
|
||||
- `uploaded_at` - TIMESTAMP NOT NULL
|
||||
- `posted` - BOOLEAN DEFAULT 0
|
||||
- `likes` - INTEGER DEFAULT 0
|
||||
|
||||
### `migrations` table (auto-created):
|
||||
- `id` - INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
- `migration_name` - TEXT NOT NULL UNIQUE
|
||||
- `applied_at` - TIMESTAMP NOT NULL
|
||||
|
||||
---
|
||||
|
||||
**Remember: Schema changes without migrations will cause deployment issues and data inconsistencies!**
|
||||
|
||||
Always follow the migration workflow for database changes.
|
||||
10
Dockerfile
10
Dockerfile
@@ -15,11 +15,13 @@ RUN pip install uv
|
||||
# Copy project files
|
||||
COPY pyproject.toml .
|
||||
COPY main.py .
|
||||
COPY templates/ templates/
|
||||
COPY app/ app/
|
||||
COPY migrate_session_changes.py .
|
||||
COPY README_MIGRATION.md .
|
||||
COPY README.md .
|
||||
|
||||
# Create uploads directory
|
||||
RUN mkdir -p static/uploads
|
||||
# Create uploads directory in the correct location
|
||||
RUN mkdir -p app/static/uploads
|
||||
|
||||
# Create and activate virtual environment, then install dependencies
|
||||
RUN uv venv && \
|
||||
@@ -30,7 +32,7 @@ RUN uv venv && \
|
||||
ENV FLASK_APP=main.py
|
||||
ENV FLASK_ENV=production
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
ENV GUNICORN_CMD_ARGS="--workers=4 --bind=0.0.0.0:5000 --timeout=120"
|
||||
ENV GUNICORN_CMD_ARGS="--workers=1 --bind=0.0.0.0:5000 --timeout=120"
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5000
|
||||
|
||||
160
README_DOCKER.md
Normal file
160
README_DOCKER.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Docker Setup for Pets of Powerwashing
|
||||
|
||||
## Updated for Refactored Architecture
|
||||
|
||||
The Docker setup has been updated to work with the refactored Flask application structure.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Build and Run
|
||||
```bash
|
||||
# Build and start the application
|
||||
docker compose up --build
|
||||
|
||||
# Or run in detached mode
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
### 2. Run Database Migration
|
||||
```bash
|
||||
# Run the migration (first time setup or after schema changes)
|
||||
docker compose --profile migrate up migrate
|
||||
```
|
||||
|
||||
### 3. Access the Application
|
||||
- **Web Interface**: http://localhost:54321
|
||||
- **Admin Login**: username: `admin`, password: `password123`
|
||||
|
||||
## File Structure Changes
|
||||
|
||||
The refactored application now uses this structure:
|
||||
```
|
||||
app/
|
||||
├── static/uploads/ # Upload directory (mounted as volume)
|
||||
├── templates/ # HTML templates
|
||||
├── models/ # Database models
|
||||
├── routes/ # Route handlers (blueprints)
|
||||
├── utils/ # Utilities and helpers
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Docker Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
You can customize the application through environment variables:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
# Flask settings
|
||||
- FLASK_ENV=production
|
||||
|
||||
# Application settings
|
||||
- UPLOAD_FOLDER=app/static/uploads
|
||||
- DATABASE_PATH=pet_pictures.db
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD=password123
|
||||
|
||||
# Optional: Override secret key for production
|
||||
- SECRET_KEY=your-secret-key-here
|
||||
```
|
||||
|
||||
### Volume Mounts
|
||||
|
||||
- `./app/static/uploads:/app/app/static/uploads` - Persistent file uploads
|
||||
- `./pet_pictures.db:/app/pet_pictures.db` - Persistent database
|
||||
|
||||
## Services
|
||||
|
||||
### Web Service (`web`)
|
||||
- **Port**: 54321 (maps to internal port 5000)
|
||||
- **Process**: Gunicorn with 4 workers
|
||||
- **Health Check**: HTTP check every 30 seconds
|
||||
- **Restart Policy**: unless-stopped
|
||||
|
||||
### Migration Service (`migrate`)
|
||||
- **Purpose**: Database schema updates
|
||||
- **Usage**: `docker compose --profile migrate up migrate`
|
||||
- **Script**: Uses `migrate_session_changes.py`
|
||||
|
||||
## Commands
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# Build only
|
||||
docker compose build
|
||||
|
||||
# View logs
|
||||
docker compose logs -f
|
||||
|
||||
# Stop services
|
||||
docker compose down
|
||||
|
||||
# Remove everything including volumes
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
### Production
|
||||
```bash
|
||||
# Start in production mode
|
||||
FLASK_ENV=production docker compose up -d
|
||||
|
||||
# Update and restart
|
||||
docker compose pull
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**1. Permission Issues with Uploads**
|
||||
```bash
|
||||
# Fix upload directory permissions
|
||||
sudo chown -R 1000:1000 ./app/static/uploads
|
||||
```
|
||||
|
||||
**2. Database Migration Fails**
|
||||
```bash
|
||||
# Run migration manually
|
||||
docker compose exec web python migrate_session_changes.py
|
||||
```
|
||||
|
||||
**3. Build Fails**
|
||||
```bash
|
||||
# Clean build
|
||||
docker compose build --no-cache
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
The application includes a health check that:
|
||||
- Tests HTTP connectivity on port 5000
|
||||
- Retries 3 times with 5-second delays
|
||||
- Runs every 30 seconds
|
||||
- Allows 40 seconds for startup
|
||||
|
||||
## Security Notes
|
||||
|
||||
**For Production:**
|
||||
1. Change default admin credentials via environment variables
|
||||
2. Set a secure `SECRET_KEY`
|
||||
3. Use HTTPS reverse proxy (nginx/traefik)
|
||||
4. Limit file upload sizes
|
||||
5. Regular database backups
|
||||
|
||||
## File Locations in Container
|
||||
|
||||
- **Application**: `/app/`
|
||||
- **Database**: `/app/pet_pictures.db`
|
||||
- **Uploads**: `/app/app/static/uploads/`
|
||||
- **Logs**: `/app/logs/` (if logging enabled)
|
||||
|
||||
## Changes from Previous Version
|
||||
|
||||
✅ **Updated paths** for refactored app structure
|
||||
✅ **New migration script** (`migrate_session_changes.py`)
|
||||
✅ **Added environment variables** for configuration
|
||||
✅ **Improved .dockerignore** for smaller builds
|
||||
✅ **Removed obsolete version** from docker-compose.yml
|
||||
✅ **Better volume mapping** for uploads directory
|
||||
@@ -15,7 +15,10 @@ def create_app():
|
||||
app.config.from_object(Config)
|
||||
|
||||
# Ensure upload directory exists
|
||||
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||
upload_path = os.path.join(app.static_folder, 'uploads')
|
||||
os.makedirs(upload_path, exist_ok=True)
|
||||
# Update config to use absolute path for file operations
|
||||
app.config['UPLOAD_FOLDER'] = upload_path
|
||||
|
||||
# Setup logging
|
||||
from app.utils.logging_config import setup_logging
|
||||
|
||||
@@ -12,7 +12,7 @@ class Config:
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or os.urandom(24)
|
||||
|
||||
# Upload settings
|
||||
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER') or 'app/static/uploads'
|
||||
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER') or 'static/uploads'
|
||||
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
|
||||
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "54321:5000"
|
||||
volumes:
|
||||
- ./static/uploads:/app/static/uploads
|
||||
- ./app/static/uploads:/app/app/static/uploads
|
||||
- ./pet_pictures.db:/app/pet_pictures.db
|
||||
environment:
|
||||
- FLASK_APP=main.py
|
||||
- FLASK_ENV=production
|
||||
- GUNICORN_CMD_ARGS=--workers=4 --bind=0.0.0.0:5000 --timeout=120 --keep-alive=5 --worker-class=sync --worker-connections=1000 --max-requests=1000 --max-requests-jitter=50
|
||||
- GUNICORN_CMD_ARGS=--workers=1 --bind=0.0.0.0:5000 --timeout=120 --keep-alive=5 --worker-class=sync --worker-connections=1000 --max-requests=1000 --max-requests-jitter=50
|
||||
# Application configuration
|
||||
- UPLOAD_FOLDER=app/static/uploads
|
||||
- DATABASE_PATH=pet_pictures.db
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD=password123
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
@@ -36,6 +39,6 @@ services:
|
||||
build: .
|
||||
volumes:
|
||||
- ./pet_pictures.db:/app/pet_pictures.db
|
||||
command: python migrate.py
|
||||
command: python migrate_session_changes.py
|
||||
profiles:
|
||||
- migrate
|
||||
|
||||
@@ -1 +1,13 @@
|
||||
2025-08-07 18:12:42,230 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
|
||||
2025-08-07 23:46:18,045 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
|
||||
2025-08-07 23:46:25,378 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
|
||||
2025-08-07 23:46:48,136 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
|
||||
2025-08-07 23:46:56,323 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
|
||||
2025-08-07 23:47:05,316 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
|
||||
2025-08-07 23:47:20,820 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
|
||||
2025-08-07 23:47:44,082 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
|
||||
2025-08-07 23:47:44,243 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
|
||||
2025-08-07 23:48:09,264 ERROR: 404 error: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/error_handlers.py:13]
|
||||
2025-08-15 07:36:43,456 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
|
||||
2025-08-15 07:36:47,429 ERROR: 404 error: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/error_handlers.py:13]
|
||||
2025-08-15 07:37:31,681 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
|
||||
|
||||
3
main.py
3
main.py
@@ -15,4 +15,5 @@ app = create_app()
|
||||
if __name__ == "__main__":
|
||||
# Development server configuration
|
||||
debug_mode = os.environ.get('FLASK_ENV') == 'development'
|
||||
app.run(debug=debug_mode, host='0.0.0.0', port=5000)
|
||||
port = int(os.environ.get('PORT', 5001)) # Use port 5001 to avoid AirPlay conflict
|
||||
app.run(debug=debug_mode, host='0.0.0.0', port=port)
|
||||
|
||||
80
migrations/001_add_description_column.py
Executable file
80
migrations/001_add_description_column.py
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration 001: Add description column
|
||||
Date: 2025-08-08
|
||||
Description: Add description field to pet_pictures table for optional image descriptions
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def check_column_exists(cursor, table_name, column_name):
|
||||
"""Check if a column exists in a table"""
|
||||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
return column_name in columns
|
||||
|
||||
|
||||
def migrate_up(cursor):
|
||||
"""Apply the migration"""
|
||||
if not check_column_exists(cursor, 'pet_pictures', 'description'):
|
||||
print("Adding description column to pet_pictures table...")
|
||||
cursor.execute("ALTER TABLE pet_pictures ADD COLUMN description TEXT")
|
||||
return True
|
||||
else:
|
||||
print("Description column already exists, skipping...")
|
||||
return False
|
||||
|
||||
|
||||
def migrate_down(cursor):
|
||||
"""Rollback the migration (SQLite doesn't support dropping columns easily)"""
|
||||
print("WARNING: Cannot rollback this migration easily in SQLite")
|
||||
print("Description column will remain but can be ignored")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the migration"""
|
||||
db_path = "pet_pictures.db"
|
||||
|
||||
try:
|
||||
db = sqlite3.connect(db_path)
|
||||
cursor = db.cursor()
|
||||
|
||||
print(f"Running migration 001: Add description column")
|
||||
print(f"Database: {db_path}")
|
||||
print(f"Timestamp: {datetime.now().isoformat()}")
|
||||
print("-" * 50)
|
||||
|
||||
if migrate_up(cursor):
|
||||
db.commit()
|
||||
print("✓ Migration 001 applied successfully")
|
||||
else:
|
||||
print("• Migration 001 already applied")
|
||||
|
||||
# Verify the migration
|
||||
cursor.execute("PRAGMA table_info(pet_pictures)")
|
||||
columns = cursor.fetchall()
|
||||
print("\nFinal schema:")
|
||||
for col in columns:
|
||||
nullable = "NOT NULL" if col[3] else "NULL"
|
||||
default = f"DEFAULT {col[4]}" if col[4] else ""
|
||||
print(f" {col[1]} {col[2]} {nullable} {default}")
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"❌ Migration failed: {e}")
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error: {e}")
|
||||
return 1
|
||||
finally:
|
||||
if 'db' in locals():
|
||||
db.close()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
142
migrations/README.md
Normal file
142
migrations/README.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# Database Migrations
|
||||
|
||||
This directory contains database migrations for the Pet Picture Queue application.
|
||||
|
||||
## Migration System
|
||||
|
||||
The migration system provides:
|
||||
- ✅ **Ordered execution** - Migrations run in filename order (001_, 002_, etc.)
|
||||
- ✅ **Tracking** - Applied migrations are tracked in the database
|
||||
- ✅ **Idempotent** - Safe to run multiple times
|
||||
- ✅ **Rollback support** - Individual migrations can implement rollback logic
|
||||
|
||||
## Usage
|
||||
|
||||
### Run All Pending Migrations
|
||||
```bash
|
||||
# From project root
|
||||
python migrations/migrate.py
|
||||
|
||||
# Or make it executable and run directly
|
||||
chmod +x migrations/migrate.py
|
||||
./migrations/migrate.py
|
||||
```
|
||||
|
||||
### Run Individual Migration
|
||||
```bash
|
||||
python migrations/001_add_description_column.py
|
||||
```
|
||||
|
||||
## Migration Structure
|
||||
|
||||
Each migration file should follow this pattern:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration XXX: Description of what this migration does
|
||||
Date: YYYY-MM-DD
|
||||
Description: Detailed explanation
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
def migrate_up(cursor):
|
||||
"""Apply the migration"""
|
||||
# Your migration code here
|
||||
cursor.execute("ALTER TABLE ...")
|
||||
return True # or False if skipped
|
||||
|
||||
def migrate_down(cursor):
|
||||
"""Rollback the migration (optional)"""
|
||||
# Rollback code here
|
||||
return True
|
||||
|
||||
def main():
|
||||
"""Run migration standalone"""
|
||||
# Standard standalone execution code
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
```
|
||||
|
||||
## Existing Migrations
|
||||
|
||||
### 001_add_description_column.py
|
||||
- **Purpose**: Add description field to pet_pictures table
|
||||
- **Date**: 2025-08-08
|
||||
- **Changes**: Adds `description TEXT NULL` column
|
||||
|
||||
## Migration Naming Convention
|
||||
|
||||
- Use 3-digit numbers: `001_`, `002_`, `003_`, etc.
|
||||
- Descriptive names: `add_description_column`, `create_user_table`
|
||||
- Full format: `XXX_descriptive_name.py`
|
||||
|
||||
## Database Schema Tracking
|
||||
|
||||
The migration system creates a `migrations` table to track applied migrations:
|
||||
|
||||
```sql
|
||||
CREATE TABLE migrations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
migration_name TEXT NOT NULL UNIQUE,
|
||||
applied_at TIMESTAMP NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always backup** before running migrations on production data
|
||||
2. **Test migrations** on development data first
|
||||
3. **Make migrations idempotent** - check if changes already exist
|
||||
4. **Keep migrations small** - one logical change per migration
|
||||
5. **Never edit** existing migration files after they've been applied
|
||||
6. **Document breaking changes** in migration comments
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Create migration**: Copy template and modify
|
||||
2. **Test locally**: Run migration on development database
|
||||
3. **Add to version control**: Commit the migration file
|
||||
4. **Deploy**: Run migrations on staging/production
|
||||
5. **Verify**: Check database schema and data integrity
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Migration fails:**
|
||||
```bash
|
||||
# Check database connection
|
||||
ls -la pet_pictures.db
|
||||
|
||||
# Check migration table
|
||||
sqlite3 pet_pictures.db "SELECT * FROM migrations;"
|
||||
|
||||
# Run specific migration for debugging
|
||||
python migrations/001_add_description_column.py
|
||||
```
|
||||
|
||||
**Reset migration tracking:**
|
||||
```sql
|
||||
-- Careful! This will re-run all migrations
|
||||
DELETE FROM migrations WHERE migration_name = '001_add_description_column';
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
### Docker
|
||||
The migration runner is integrated with Docker:
|
||||
```bash
|
||||
# Run migrations in Docker
|
||||
docker compose --profile migrate up migrate
|
||||
```
|
||||
|
||||
### CI/CD
|
||||
Add to deployment scripts:
|
||||
```bash
|
||||
# Run migrations before starting app
|
||||
python migrations/migrate.py && python main.py
|
||||
```
|
||||
1
migrations/__init__.py
Normal file
1
migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Migrations package
|
||||
174
migrations/migrate.py
Executable file
174
migrations/migrate.py
Executable file
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration runner for Pet Picture Queue
|
||||
Handles database schema migrations in order
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import importlib.util
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def get_database_path():
|
||||
"""Get the correct database path"""
|
||||
if os.path.exists("pet_pictures.db"):
|
||||
return "pet_pictures.db"
|
||||
elif os.path.exists("app/pet_pictures.db"):
|
||||
return "app/pet_pictures.db"
|
||||
else:
|
||||
return "pet_pictures.db"
|
||||
|
||||
|
||||
def create_migration_table(cursor):
|
||||
"""Create migrations tracking table"""
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
migration_name TEXT NOT NULL UNIQUE,
|
||||
applied_at TIMESTAMP NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
def get_applied_migrations(cursor):
|
||||
"""Get list of applied migrations"""
|
||||
try:
|
||||
cursor.execute("SELECT migration_name FROM migrations ORDER BY id")
|
||||
return [row[0] for row in cursor.fetchall()]
|
||||
except sqlite3.OperationalError:
|
||||
# Migrations table doesn't exist yet
|
||||
return []
|
||||
|
||||
|
||||
def get_migration_files():
|
||||
"""Get list of migration files in order"""
|
||||
migrations_dir = os.path.dirname(__file__)
|
||||
migration_files = []
|
||||
|
||||
for file in os.listdir(migrations_dir):
|
||||
if file.endswith('.py') and file != 'migrate.py' and file != '__init__.py':
|
||||
migration_files.append(file)
|
||||
|
||||
# Sort by filename (assumes numbered naming like 001_xxx.py)
|
||||
migration_files.sort()
|
||||
return migration_files
|
||||
|
||||
|
||||
def load_migration_module(migration_file):
|
||||
"""Load a migration module dynamically"""
|
||||
migrations_dir = os.path.dirname(__file__)
|
||||
file_path = os.path.join(migrations_dir, migration_file)
|
||||
|
||||
spec = importlib.util.spec_from_file_location("migration", file_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
return module
|
||||
|
||||
|
||||
def run_migrations():
|
||||
"""Run pending migrations"""
|
||||
db_path = get_database_path()
|
||||
print(f"Using database: {db_path}")
|
||||
|
||||
try:
|
||||
db = sqlite3.connect(db_path)
|
||||
cursor = db.cursor()
|
||||
|
||||
# Create migrations table
|
||||
create_migration_table(cursor)
|
||||
db.commit()
|
||||
|
||||
# Get current state
|
||||
applied_migrations = get_applied_migrations(cursor)
|
||||
available_migrations = get_migration_files()
|
||||
|
||||
print(f"Applied migrations: {len(applied_migrations)}")
|
||||
print(f"Available migrations: {len(available_migrations)}")
|
||||
|
||||
# Find pending migrations
|
||||
pending_migrations = []
|
||||
for migration_file in available_migrations:
|
||||
migration_name = migration_file.replace('.py', '')
|
||||
if migration_name not in applied_migrations:
|
||||
pending_migrations.append(migration_file)
|
||||
|
||||
if not pending_migrations:
|
||||
print("✓ No pending migrations")
|
||||
return 0
|
||||
|
||||
print(f"\nRunning {len(pending_migrations)} pending migrations:")
|
||||
|
||||
# Run each pending migration
|
||||
for migration_file in pending_migrations:
|
||||
migration_name = migration_file.replace('.py', '')
|
||||
print(f"\n📦 Running migration: {migration_name}")
|
||||
|
||||
try:
|
||||
# Load and run migration
|
||||
module = load_migration_module(migration_file)
|
||||
|
||||
if hasattr(module, 'migrate_up'):
|
||||
success = module.migrate_up(cursor)
|
||||
if success is not False: # Allow None or True
|
||||
# Mark as applied
|
||||
cursor.execute(
|
||||
"INSERT INTO migrations (migration_name, applied_at) VALUES (?, ?)",
|
||||
(migration_name, datetime.now())
|
||||
)
|
||||
db.commit()
|
||||
print(f"✓ Migration {migration_name} applied successfully")
|
||||
else:
|
||||
print(f"• Migration {migration_name} skipped (already applied)")
|
||||
else:
|
||||
print(f"❌ Migration {migration_name} has no migrate_up function")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Migration {migration_name} failed: {e}")
|
||||
db.rollback()
|
||||
return 1
|
||||
|
||||
print(f"\n🎉 All migrations completed successfully!")
|
||||
|
||||
# Show final schema
|
||||
cursor.execute("PRAGMA table_info(pet_pictures)")
|
||||
columns = cursor.fetchall()
|
||||
print(f"\nFinal pet_pictures schema:")
|
||||
for col in columns:
|
||||
nullable = "NOT NULL" if col[3] else "NULL"
|
||||
default = f"DEFAULT {col[4]}" if col[4] else ""
|
||||
print(f" {col[1]} {col[2]} {nullable} {default}")
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Migration runner failed: {e}")
|
||||
return 1
|
||||
finally:
|
||||
if 'db' in locals():
|
||||
db.close()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
print("=" * 60)
|
||||
print("Pet Picture Queue - Database Migration Runner")
|
||||
print(f"Started at: {datetime.now().isoformat()}")
|
||||
print("=" * 60)
|
||||
|
||||
result = run_migrations()
|
||||
|
||||
print("=" * 60)
|
||||
if result == 0:
|
||||
print("✅ Migration process completed successfully!")
|
||||
else:
|
||||
print("❌ Migration process failed!")
|
||||
print("=" * 60)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user