Add Containerfle
This commit is contained in:
parent
fa37916440
commit
e354caa03f
133
.dockerignore
Normal file
133
.dockerignore
Normal file
@ -0,0 +1,133 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
*.md
|
||||
docs/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.venv/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Database files (will be created in container)
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Data directories
|
||||
data/
|
||||
temp/
|
||||
tmp/
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*.yml
|
||||
.dockerignore
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
.env.development
|
||||
|
||||
# Node modules (if any)
|
||||
node_modules/
|
||||
|
||||
# Coverage reports
|
||||
htmlcov/
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage.xml
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.tox/
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
Pipfile.lock
|
||||
|
||||
# PEP 582
|
||||
__pypackages__/
|
||||
|
||||
# Celery
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
76
Containerfile
Normal file
76
Containerfile
Normal file
@ -0,0 +1,76 @@
|
||||
# Multi-stage Containerfile for Hockey Results Application
|
||||
# Supports multiple database backends (PostgreSQL, MariaDB, SQLite)
|
||||
|
||||
# Stage 1: Build stage
|
||||
FROM python:3.11-slim as builder
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
# Install system dependencies for building Python packages
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
default-libmysqlclient-dev \
|
||||
pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create and activate virtual environment
|
||||
RUN python -m venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
# Copy requirements and install Python dependencies
|
||||
COPY requirements.txt motm_app/requirements.txt ./
|
||||
RUN pip install --upgrade pip && \
|
||||
pip install -r requirements.txt && \
|
||||
pip install -r motm_app/requirements.txt
|
||||
|
||||
# Stage 2: Runtime stage
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH="/opt/venv/bin:$PATH" \
|
||||
DATABASE_TYPE=sqlite \
|
||||
FLASK_ENV=production \
|
||||
FLASK_APP=motm_app/main.py
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libpq5 \
|
||||
default-mysql-client \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy virtual environment from builder stage
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
|
||||
# Create non-root user
|
||||
RUN groupadd -r appuser && useradd -r -g appuser appuser
|
||||
|
||||
# Create application directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy application code
|
||||
COPY --chown=appuser:appuser . .
|
||||
|
||||
# Create directories for data and logs
|
||||
RUN mkdir -p /app/data /app/logs && \
|
||||
chown -R appuser:appuser /app/data /app/logs
|
||||
|
||||
# Switch to non-root user
|
||||
USER appuser
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:5000/ || exit 1
|
||||
|
||||
# Default command
|
||||
CMD ["python", "motm_app/main.py"]
|
||||
362
DOCKER.md
Normal file
362
DOCKER.md
Normal file
@ -0,0 +1,362 @@
|
||||
# Docker Containerization Guide
|
||||
|
||||
This guide explains how to run the Hockey Results Application using Docker containers with support for multiple database backends.
|
||||
|
||||
## 🏒 Quick Start
|
||||
|
||||
### Using Docker Compose (Recommended)
|
||||
|
||||
```bash
|
||||
# Start with PostgreSQL (default)
|
||||
docker-compose up -d
|
||||
|
||||
# Start with MariaDB
|
||||
docker-compose --profile mariadb up -d
|
||||
|
||||
# Start with SQLite (no database container needed)
|
||||
DATABASE_TYPE=sqlite docker-compose up -d
|
||||
```
|
||||
|
||||
### Using Docker Commands
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
./docker/build.sh
|
||||
|
||||
# Run with SQLite
|
||||
./docker/run.sh
|
||||
|
||||
# Run with PostgreSQL
|
||||
./docker/run.sh -d postgresql
|
||||
|
||||
# Run with MariaDB
|
||||
./docker/run.sh -d mysql
|
||||
```
|
||||
|
||||
## 🗄️ Database Options
|
||||
|
||||
### 1. SQLite (Default)
|
||||
- **Pros**: No external dependencies, perfect for development
|
||||
- **Cons**: Not suitable for production with multiple users
|
||||
- **Use case**: Development, testing, single-user deployments
|
||||
|
||||
```bash
|
||||
# Using docker-compose
|
||||
DATABASE_TYPE=sqlite docker-compose up -d
|
||||
|
||||
# Using docker run
|
||||
./docker/run.sh -d sqlite
|
||||
```
|
||||
|
||||
### 2. PostgreSQL (Recommended for Production)
|
||||
- **Pros**: Robust, ACID compliant, excellent performance
|
||||
- **Cons**: Requires external database container
|
||||
- **Use case**: Production deployments, multi-user applications
|
||||
|
||||
```bash
|
||||
# Using docker-compose (default)
|
||||
docker-compose up -d
|
||||
|
||||
# Using docker run (requires external PostgreSQL)
|
||||
./docker/run.sh -d postgresql
|
||||
```
|
||||
|
||||
### 3. MariaDB/MySQL
|
||||
- **Pros**: Widely supported, good performance
|
||||
- **Cons**: Requires external database container
|
||||
- **Use case**: Legacy systems, specific MySQL requirements
|
||||
|
||||
```bash
|
||||
# Using docker-compose
|
||||
docker-compose --profile mariadb up -d
|
||||
|
||||
# Using docker run (requires external MariaDB)
|
||||
./docker/run.sh -d mysql
|
||||
```
|
||||
|
||||
## 🚀 Deployment Options
|
||||
|
||||
### Development Environment
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repository-url>
|
||||
cd gcp-hockey-results
|
||||
|
||||
# Start with SQLite for development
|
||||
./docker/run.sh -d sqlite -p 5000:5000
|
||||
|
||||
# Access the application
|
||||
open http://localhost:5000
|
||||
```
|
||||
|
||||
### Production Environment
|
||||
|
||||
#### Option 1: Docker Compose (Single Server)
|
||||
|
||||
```bash
|
||||
# Set production environment variables
|
||||
export SECRET_KEY="your-production-secret-key"
|
||||
export BASIC_AUTH_PASSWORD="strong-production-password"
|
||||
export POSTGRES_PASSWORD="strong-database-password"
|
||||
|
||||
# Start production stack
|
||||
docker-compose up -d
|
||||
|
||||
# Scale the application (optional)
|
||||
docker-compose up -d --scale hockey-app=3
|
||||
```
|
||||
|
||||
#### Option 2: Kubernetes (Multi-Server)
|
||||
|
||||
```yaml
|
||||
# kubernetes/deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: hockey-app
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: hockey-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: hockey-app
|
||||
spec:
|
||||
containers:
|
||||
- name: hockey-app
|
||||
image: hockey-results:latest
|
||||
ports:
|
||||
- containerPort: 5000
|
||||
env:
|
||||
- name: DATABASE_TYPE
|
||||
value: "postgresql"
|
||||
- name: POSTGRES_HOST
|
||||
value: "postgres-service"
|
||||
```
|
||||
|
||||
#### Option 3: Cloud Platforms
|
||||
|
||||
**Google Cloud Run:**
|
||||
```bash
|
||||
# Build and push to Google Container Registry
|
||||
./docker/build.sh --registry gcr.io/your-project-id --push
|
||||
|
||||
# Deploy to Cloud Run
|
||||
gcloud run deploy hockey-app \
|
||||
--image gcr.io/your-project-id/hockey-results:latest \
|
||||
--platform managed \
|
||||
--region us-central1 \
|
||||
--set-env-vars DATABASE_TYPE=postgresql
|
||||
```
|
||||
|
||||
**AWS ECS:**
|
||||
```bash
|
||||
# Build and push to ECR
|
||||
./docker/build.sh --registry your-account.dkr.ecr.region.amazonaws.com --push
|
||||
|
||||
# Create ECS task definition with the image
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `DATABASE_TYPE` | `sqlite` | Database type: sqlite, postgresql, mysql |
|
||||
| `SECRET_KEY` | `your-secret-key-change-in-production` | Flask secret key |
|
||||
| `FLASK_ENV` | `production` | Flask environment |
|
||||
| `BASIC_AUTH_USERNAME` | `admin` | Basic auth username |
|
||||
| `BASIC_AUTH_PASSWORD` | `letmein` | Basic auth password |
|
||||
|
||||
### PostgreSQL Configuration
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `POSTGRES_HOST` | `postgres` | PostgreSQL host |
|
||||
| `POSTGRES_PORT` | `5432` | PostgreSQL port |
|
||||
| `POSTGRES_DATABASE` | `hockey_results` | Database name |
|
||||
| `POSTGRES_USER` | `hockey_user` | Database user |
|
||||
| `POSTGRES_PASSWORD` | `hockey_password` | Database password |
|
||||
|
||||
### MySQL/MariaDB Configuration
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `MYSQL_HOST` | `mariadb` | MySQL host |
|
||||
| `MYSQL_PORT` | `3306` | MySQL port |
|
||||
| `MYSQL_DATABASE` | `hockey_results` | Database name |
|
||||
| `MYSQL_USER` | `hockey_user` | Database user |
|
||||
| `MYSQL_PASSWORD` | `hockey_password` | Database password |
|
||||
|
||||
## 📊 Monitoring and Logs
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker-compose logs -f hockey-app
|
||||
|
||||
# Docker Run
|
||||
docker logs -f hockey-results-app
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
```bash
|
||||
# Check container health
|
||||
docker ps
|
||||
|
||||
# Manual health check
|
||||
curl -f http://localhost:5000/ || echo "Application is not healthy"
|
||||
```
|
||||
|
||||
### Database Connection Test
|
||||
```bash
|
||||
# Test PostgreSQL connection
|
||||
docker exec hockey-results-app python -c "
|
||||
from motm_app.database import db_config
|
||||
print('Database URL:', db_config.database_url)
|
||||
"
|
||||
|
||||
# Test application database initialization
|
||||
docker exec hockey-results-app python -c "
|
||||
from motm_app.database import init_database
|
||||
init_database()
|
||||
print('Database initialized successfully')
|
||||
"
|
||||
```
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
### Local Development with Docker
|
||||
```bash
|
||||
# Build development image
|
||||
./docker/build.sh -t dev
|
||||
|
||||
# Run with volume mount for live code changes
|
||||
docker run -it --rm \
|
||||
-p 5000:5000 \
|
||||
-v $(pwd):/app \
|
||||
-e FLASK_ENV=development \
|
||||
-e FLASK_DEBUG=true \
|
||||
hockey-results:dev
|
||||
```
|
||||
|
||||
### Database Migrations
|
||||
```bash
|
||||
# Run migrations inside container
|
||||
docker exec hockey-results-app python -c "
|
||||
from motm_app.database import init_database
|
||||
init_database()
|
||||
"
|
||||
```
|
||||
|
||||
### Backup and Restore
|
||||
```bash
|
||||
# Backup PostgreSQL data
|
||||
docker exec hockey-postgres pg_dump -U hockey_user hockey_results > backup.sql
|
||||
|
||||
# Restore PostgreSQL data
|
||||
docker exec -i hockey-postgres psql -U hockey_user hockey_results < backup.sql
|
||||
|
||||
# Backup SQLite data
|
||||
docker cp hockey-results-app:/app/data/hockey_results.db ./backup.db
|
||||
```
|
||||
|
||||
## 🔒 Security Considerations
|
||||
|
||||
### Production Security
|
||||
1. **Change default passwords**:
|
||||
```bash
|
||||
export SECRET_KEY="$(openssl rand -hex 32)"
|
||||
export BASIC_AUTH_PASSWORD="$(openssl rand -base64 32)"
|
||||
export POSTGRES_PASSWORD="$(openssl rand -base64 32)"
|
||||
```
|
||||
|
||||
2. **Use secrets management**:
|
||||
```yaml
|
||||
# docker-compose.prod.yml
|
||||
services:
|
||||
hockey-app:
|
||||
environment:
|
||||
- SECRET_KEY_FILE=/run/secrets/secret_key
|
||||
secrets:
|
||||
- secret_key
|
||||
|
||||
secrets:
|
||||
secret_key:
|
||||
file: ./secrets/secret_key.txt
|
||||
```
|
||||
|
||||
3. **Network security**:
|
||||
```yaml
|
||||
# Use custom networks
|
||||
networks:
|
||||
hockey-network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
```
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Container won't start:**
|
||||
```bash
|
||||
# Check logs
|
||||
docker logs hockey-results-app
|
||||
|
||||
# Check if port is already in use
|
||||
netstat -tlnp | grep :5000
|
||||
```
|
||||
|
||||
**Database connection issues:**
|
||||
```bash
|
||||
# Check database container status
|
||||
docker-compose ps
|
||||
|
||||
# Test database connectivity
|
||||
docker exec hockey-results-app python -c "
|
||||
import psycopg2
|
||||
conn = psycopg2.connect(host='postgres', port=5432, database='hockey_results', user='hockey_user', password='hockey_password')
|
||||
print('Database connected successfully')
|
||||
"
|
||||
```
|
||||
|
||||
**Permission issues:**
|
||||
```bash
|
||||
# Fix file permissions
|
||||
sudo chown -R $USER:$USER ./data
|
||||
chmod -R 755 ./data
|
||||
```
|
||||
|
||||
### Performance Tuning
|
||||
|
||||
**Database Optimization:**
|
||||
```bash
|
||||
# PostgreSQL tuning
|
||||
docker exec hockey-postgres psql -U hockey_user hockey_results -c "
|
||||
ALTER SYSTEM SET shared_buffers = '256MB';
|
||||
ALTER SYSTEM SET effective_cache_size = '1GB';
|
||||
SELECT pg_reload_conf();
|
||||
"
|
||||
```
|
||||
|
||||
**Application Scaling:**
|
||||
```bash
|
||||
# Scale application containers
|
||||
docker-compose up -d --scale hockey-app=3
|
||||
|
||||
# Use load balancer
|
||||
docker-compose --profile nginx up -d
|
||||
```
|
||||
|
||||
## 📚 Additional Resources
|
||||
|
||||
- [Docker Documentation](https://docs.docker.com/)
|
||||
- [Docker Compose Documentation](https://docs.docker.com/compose/)
|
||||
- [PostgreSQL Docker Image](https://hub.docker.com/_/postgres)
|
||||
- [MariaDB Docker Image](https://hub.docker.com/_/mariadb)
|
||||
- [SQLAlchemy Documentation](https://docs.sqlalchemy.org/)
|
||||
125
docker-compose.yml
Normal file
125
docker-compose.yml
Normal file
@ -0,0 +1,125 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Hockey Results Application
|
||||
hockey-app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Containerfile
|
||||
container_name: hockey-results-app
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- DATABASE_TYPE=postgresql
|
||||
- POSTGRES_HOST=postgres
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DATABASE=hockey_results
|
||||
- POSTGRES_USER=hockey_user
|
||||
- POSTGRES_PASSWORD=hockey_password
|
||||
- FLASK_ENV=production
|
||||
- SECRET_KEY=your-secret-key-change-in-production
|
||||
- BASIC_AUTH_USERNAME=admin
|
||||
- BASIC_AUTH_PASSWORD=letmein
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- hockey-network
|
||||
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: hockey-postgres
|
||||
environment:
|
||||
- POSTGRES_DB=hockey_results
|
||||
- POSTGRES_USER=hockey_user
|
||||
- POSTGRES_PASSWORD=hockey_password
|
||||
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./init-scripts:/docker-entrypoint-initdb.d
|
||||
ports:
|
||||
- "5432:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U hockey_user -d hockey_results"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- hockey-network
|
||||
|
||||
# MariaDB Database (Alternative)
|
||||
mariadb:
|
||||
image: mariadb:10.11
|
||||
container_name: hockey-mariadb
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=root_password
|
||||
- MYSQL_DATABASE=hockey_results
|
||||
- MYSQL_USER=hockey_user
|
||||
- MYSQL_PASSWORD=hockey_password
|
||||
- MYSQL_CHARSET=utf8mb4
|
||||
- MYSQL_COLLATION=utf8mb4_unicode_ci
|
||||
volumes:
|
||||
- mariadb_data:/var/lib/mysql
|
||||
- ./init-scripts:/docker-entrypoint-initdb.d
|
||||
ports:
|
||||
- "3306:3306"
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "hockey_user", "-p$$MYSQL_PASSWORD"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- hockey-network
|
||||
profiles:
|
||||
- mariadb
|
||||
|
||||
# Redis for caching (optional)
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: hockey-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- hockey-network
|
||||
profiles:
|
||||
- redis
|
||||
|
||||
# Nginx reverse proxy (optional)
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: hockey-nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./ssl:/etc/nginx/ssl:ro
|
||||
depends_on:
|
||||
- hockey-app
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- hockey-network
|
||||
profiles:
|
||||
- nginx
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
mariadb_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
hockey-network:
|
||||
driver: bridge
|
||||
127
docker/build.sh
Normal file
127
docker/build.sh
Normal file
@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
# Docker build script for Hockey Results Application
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
IMAGE_NAME="hockey-results"
|
||||
IMAGE_TAG="latest"
|
||||
FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
|
||||
echo -e "${BLUE}🏒 Building Hockey Results Application Docker Image${NC}"
|
||||
echo -e "${BLUE}================================================${NC}"
|
||||
|
||||
# Function to display usage
|
||||
usage() {
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -t, --tag TAG Set image tag (default: latest)"
|
||||
echo " -n, --name NAME Set image name (default: hockey-results)"
|
||||
echo " --no-cache Build without cache"
|
||||
echo " --push Push image to registry after build"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Build with default settings"
|
||||
echo " $0 -t v1.0.0 # Build with specific tag"
|
||||
echo " $0 --no-cache # Build without cache"
|
||||
echo " $0 -t v1.0.0 --push # Build and push to registry"
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
PUSH_IMAGE=false
|
||||
NO_CACHE=""
|
||||
REGISTRY=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-t|--tag)
|
||||
IMAGE_TAG="$2"
|
||||
FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
shift 2
|
||||
;;
|
||||
-n|--name)
|
||||
IMAGE_NAME="$2"
|
||||
FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
shift 2
|
||||
;;
|
||||
--no-cache)
|
||||
NO_CACHE="--no-cache"
|
||||
shift
|
||||
;;
|
||||
--push)
|
||||
PUSH_IMAGE=true
|
||||
shift
|
||||
;;
|
||||
--registry)
|
||||
REGISTRY="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown option: $1${NC}"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo -e "${YELLOW}📋 Build Configuration:${NC}"
|
||||
echo -e " Image Name: ${GREEN}${FULL_IMAGE_NAME}${NC}"
|
||||
echo -e " No Cache: ${GREEN}${NO_CACHE:-false}${NC}"
|
||||
echo -e " Push Image: ${GREEN}${PUSH_IMAGE}${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if Docker is running
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
echo -e "${RED}❌ Docker is not running. Please start Docker and try again.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build the image
|
||||
echo -e "${BLUE}🔨 Building Docker image...${NC}"
|
||||
if docker build $NO_CACHE -t "$FULL_IMAGE_NAME" .; then
|
||||
echo -e "${GREEN}✅ Docker image built successfully: ${FULL_IMAGE_NAME}${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Docker build failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show image information
|
||||
echo -e "${BLUE}📊 Image Information:${NC}"
|
||||
docker images "$FULL_IMAGE_NAME"
|
||||
|
||||
# Push image if requested
|
||||
if [ "$PUSH_IMAGE" = true ]; then
|
||||
echo -e "${BLUE}📤 Pushing image to registry...${NC}"
|
||||
|
||||
if [ -n "$REGISTRY" ]; then
|
||||
FULL_REGISTRY_NAME="${REGISTRY}/${FULL_IMAGE_NAME}"
|
||||
docker tag "$FULL_IMAGE_NAME" "$FULL_REGISTRY_NAME"
|
||||
docker push "$FULL_REGISTRY_NAME"
|
||||
echo -e "${GREEN}✅ Image pushed to registry: ${FULL_REGISTRY_NAME}${NC}"
|
||||
else
|
||||
docker push "$FULL_IMAGE_NAME"
|
||||
echo -e "${GREEN}✅ Image pushed to registry: ${FULL_IMAGE_NAME}${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}🎉 Build process completed successfully!${NC}"
|
||||
|
||||
# Show next steps
|
||||
echo -e "${YELLOW}📝 Next Steps:${NC}"
|
||||
echo -e " To run the container: ${BLUE}docker run -p 5000:5000 ${FULL_IMAGE_NAME}${NC}"
|
||||
echo -e " To run with docker-compose: ${BLUE}docker-compose up${NC}"
|
||||
echo -e " To run with PostgreSQL: ${BLUE}docker-compose --profile postgres up${NC}"
|
||||
echo -e " To run with MariaDB: ${BLUE}docker-compose --profile mariadb up${NC}"
|
||||
192
docker/run.sh
Normal file
192
docker/run.sh
Normal file
@ -0,0 +1,192 @@
|
||||
#!/bin/bash
|
||||
# Docker run script for Hockey Results Application
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Default configuration
|
||||
IMAGE_NAME="hockey-results:latest"
|
||||
CONTAINER_NAME="hockey-results-app"
|
||||
PORT="5000:5000"
|
||||
DATABASE_TYPE="sqlite"
|
||||
VOLUME_MOUNT=""
|
||||
|
||||
# Function to display usage
|
||||
usage() {
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -i, --image IMAGE Docker image name (default: hockey-results:latest)"
|
||||
echo " -c, --container NAME Container name (default: hockey-results-app)"
|
||||
echo " -p, --port PORT Port mapping (default: 5000:5000)"
|
||||
echo " -d, --database TYPE Database type: sqlite, postgresql, mysql (default: sqlite)"
|
||||
echo " -v, --volume PATH Mount volume for data persistence"
|
||||
echo " --detach Run container in background"
|
||||
echo " --rm Remove container when it exits"
|
||||
echo " --env KEY=VALUE Set environment variable"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Database Types:"
|
||||
echo " sqlite Use SQLite database (default, no external dependencies)"
|
||||
echo " postgresql Use PostgreSQL database"
|
||||
echo " mysql Use MySQL/MariaDB database"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Run with SQLite"
|
||||
echo " $0 -d postgresql # Run with PostgreSQL"
|
||||
echo " $0 -v ./data:/app/data # Run with data persistence"
|
||||
echo " $0 -p 8080:5000 # Run on different port"
|
||||
echo " $0 --detach # Run in background"
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
DETACH=""
|
||||
REMOVE=""
|
||||
ENV_VARS=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i|--image)
|
||||
IMAGE_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c|--container)
|
||||
CONTAINER_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
-p|--port)
|
||||
PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
-d|--database)
|
||||
DATABASE_TYPE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-v|--volume)
|
||||
VOLUME_MOUNT="-v $2"
|
||||
shift 2
|
||||
;;
|
||||
--detach)
|
||||
DETACH="-d"
|
||||
shift
|
||||
;;
|
||||
--rm)
|
||||
REMOVE="--rm"
|
||||
shift
|
||||
;;
|
||||
--env)
|
||||
ENV_VARS="$ENV_VARS -e $2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown option: $1${NC}"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo -e "${BLUE}🏒 Running Hockey Results Application${NC}"
|
||||
echo -e "${BLUE}====================================${NC}"
|
||||
|
||||
# Check if Docker is running
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
echo -e "${RED}❌ Docker is not running. Please start Docker and try again.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if image exists
|
||||
if ! docker images -q "$IMAGE_NAME" | grep -q .; then
|
||||
echo -e "${YELLOW}⚠️ Image $IMAGE_NAME not found. Building it first...${NC}"
|
||||
./docker/build.sh -n "$(echo $IMAGE_NAME | cut -d':' -f1)" -t "$(echo $IMAGE_NAME | cut -d':' -f2)"
|
||||
fi
|
||||
|
||||
# Stop existing container if running
|
||||
if docker ps -q -f name="$CONTAINER_NAME" | grep -q .; then
|
||||
echo -e "${YELLOW}🛑 Stopping existing container...${NC}"
|
||||
docker stop "$CONTAINER_NAME"
|
||||
fi
|
||||
|
||||
# Remove existing container if it exists
|
||||
if docker ps -aq -f name="$CONTAINER_NAME" | grep -q .; then
|
||||
echo -e "${YELLOW}🗑️ Removing existing container...${NC}"
|
||||
docker rm "$CONTAINER_NAME"
|
||||
fi
|
||||
|
||||
# Set environment variables based on database type
|
||||
case $DATABASE_TYPE in
|
||||
"postgresql")
|
||||
ENV_VARS="$ENV_VARS -e DATABASE_TYPE=postgresql"
|
||||
ENV_VARS="$ENV_VARS -e POSTGRES_HOST=host.docker.internal"
|
||||
ENV_VARS="$ENV_VARS -e POSTGRES_PORT=5432"
|
||||
ENV_VARS="$ENV_VARS -e POSTGRES_DATABASE=hockey_results"
|
||||
ENV_VARS="$ENV_VARS -e POSTGRES_USER=hockey_user"
|
||||
ENV_VARS="$ENV_VARS -e POSTGRES_PASSWORD=hockey_password"
|
||||
echo -e "${YELLOW}📊 Using PostgreSQL database${NC}"
|
||||
;;
|
||||
"mysql")
|
||||
ENV_VARS="$ENV_VARS -e DATABASE_TYPE=mysql"
|
||||
ENV_VARS="$ENV_VARS -e MYSQL_HOST=host.docker.internal"
|
||||
ENV_VARS="$ENV_VARS -e MYSQL_PORT=3306"
|
||||
ENV_VARS="$ENV_VARS -e MYSQL_DATABASE=hockey_results"
|
||||
ENV_VARS="$ENV_VARS -e MYSQL_USER=hockey_user"
|
||||
ENV_VARS="$ENV_VARS -e MYSQL_PASSWORD=hockey_password"
|
||||
echo -e "${YELLOW}📊 Using MySQL/MariaDB database${NC}"
|
||||
;;
|
||||
"sqlite")
|
||||
ENV_VARS="$ENV_VARS -e DATABASE_TYPE=sqlite"
|
||||
ENV_VARS="$ENV_VARS -e SQLITE_DATABASE_PATH=/app/data/hockey_results.db"
|
||||
VOLUME_MOUNT="$VOLUME_MOUNT -v $(pwd)/data:/app/data"
|
||||
echo -e "${YELLOW}📊 Using SQLite database${NC}"
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}❌ Unsupported database type: $DATABASE_TYPE${NC}"
|
||||
echo -e "Supported types: sqlite, postgresql, mysql"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "${BLUE}🚀 Starting container...${NC}"
|
||||
echo -e " Image: ${GREEN}$IMAGE_NAME${NC}"
|
||||
echo -e " Container: ${GREEN}$CONTAINER_NAME${NC}"
|
||||
echo -e " Port: ${GREEN}$PORT${NC}"
|
||||
echo -e " Database: ${GREEN}$DATABASE_TYPE${NC}"
|
||||
|
||||
# Run the container
|
||||
docker run \
|
||||
$DETACH \
|
||||
$REMOVE \
|
||||
--name "$CONTAINER_NAME" \
|
||||
-p "$PORT" \
|
||||
$VOLUME_MOUNT \
|
||||
$ENV_VARS \
|
||||
"$IMAGE_NAME"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
if [ -n "$DETACH" ]; then
|
||||
echo -e "${GREEN}✅ Container started successfully in background${NC}"
|
||||
echo -e "${BLUE}📝 Container Information:${NC}"
|
||||
docker ps -f name="$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}📋 Useful Commands:${NC}"
|
||||
echo -e " View logs: ${BLUE}docker logs $CONTAINER_NAME${NC}"
|
||||
echo -e " Stop container: ${BLUE}docker stop $CONTAINER_NAME${NC}"
|
||||
echo -e " Remove container: ${BLUE}docker rm $CONTAINER_NAME${NC}"
|
||||
echo -e " Access shell: ${BLUE}docker exec -it $CONTAINER_NAME /bin/bash${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✅ Container finished${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}❌ Failed to start container${NC}"
|
||||
exit 1
|
||||
fi
|
||||
68
docker/start.sh
Normal file
68
docker/start.sh
Normal file
@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
# Docker startup script for Hockey Results Application
|
||||
|
||||
set -e
|
||||
|
||||
echo "🏒 Starting Hockey Results Application..."
|
||||
|
||||
# Function to wait for database
|
||||
wait_for_database() {
|
||||
local db_type=$1
|
||||
local max_attempts=30
|
||||
local attempt=1
|
||||
|
||||
echo "⏳ Waiting for $db_type database to be ready..."
|
||||
|
||||
case $db_type in
|
||||
"postgresql")
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if python -c "import psycopg2; psycopg2.connect(host='$POSTGRES_HOST', port='$POSTGRES_PORT', database='$POSTGRES_DATABASE', user='$POSTGRES_USER', password='$POSTGRES_PASSWORD')" 2>/dev/null; then
|
||||
echo "✅ PostgreSQL database is ready!"
|
||||
return 0
|
||||
fi
|
||||
echo "Attempt $attempt/$max_attempts: Database not ready, waiting..."
|
||||
sleep 2
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
;;
|
||||
"mysql"|"mariadb")
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if python -c "import pymysql; pymysql.connect(host='$MYSQL_HOST', port='$MYSQL_PORT', database='$MYSQL_DATABASE', user='$MYSQL_USER', password='$MYSQL_PASSWORD')" 2>/dev/null; then
|
||||
echo "✅ MySQL/MariaDB database is ready!"
|
||||
return 0
|
||||
fi
|
||||
echo "Attempt $attempt/$max_attempts: Database not ready, waiting..."
|
||||
sleep 2
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "❌ Database connection timeout after $max_attempts attempts"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Initialize database if needed
|
||||
init_database() {
|
||||
echo "🔧 Initializing database..."
|
||||
python -c "
|
||||
from motm_app.database import init_database
|
||||
try:
|
||||
init_database()
|
||||
print('✅ Database initialized successfully')
|
||||
except Exception as e:
|
||||
print(f'⚠️ Database initialization warning: {e}')
|
||||
"
|
||||
}
|
||||
|
||||
# Wait for database if not using SQLite
|
||||
if [ "$DATABASE_TYPE" != "sqlite" ]; then
|
||||
wait_for_database "$DATABASE_TYPE"
|
||||
fi
|
||||
|
||||
# Initialize database
|
||||
init_database
|
||||
|
||||
# Start the application
|
||||
echo "🚀 Starting Flask application..."
|
||||
exec python motm_app/main.py
|
||||
112
motm_app/alembic.ini
Normal file
112
motm_app/alembic.ini
Normal file
@ -0,0 +1,112 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = migrations
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python-dateutil library that can be
|
||||
# installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version number format
|
||||
version_num_format = %04d
|
||||
|
||||
# version path separator; As mentioned above, this is the character used to split
|
||||
# version_locations. The default within new alembic.ini files is "os", which uses
|
||||
# os.pathsep. If this key is omitted entirely, it falls back to the legacy
|
||||
# behavior of splitting on spaces and/or commas.
|
||||
# Valid values for version_path_separator are:
|
||||
#
|
||||
# version_path_separator = :
|
||||
# version_path_separator = ;
|
||||
# version_path_separator = space
|
||||
version_path_separator = os
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = sqlite:///hockey_results.db
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = %(here)s/.venv/bin/ruff
|
||||
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
292
motm_app/database.py
Normal file
292
motm_app/database.py
Normal file
@ -0,0 +1,292 @@
|
||||
# encoding=utf-8
|
||||
"""
|
||||
Database configuration and models for multi-database support using SQLAlchemy.
|
||||
Supports PostgreSQL, MariaDB/MySQL, and SQLite.
|
||||
"""
|
||||
|
||||
import os
|
||||
from sqlalchemy import create_engine, Column, Integer, String, Date, DateTime, Text, SmallInteger, ForeignKey, Boolean, Float
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, relationship
|
||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||
from sqlalchemy.dialects.postgresql import JSON
|
||||
from datetime import datetime
|
||||
|
||||
# Base class for all models
|
||||
Base = declarative_base()
|
||||
|
||||
class DatabaseConfig:
|
||||
"""Database configuration class for multiple database support."""
|
||||
|
||||
def __init__(self):
|
||||
self.database_url = self._get_database_url()
|
||||
self.engine = create_engine(self.database_url, echo=False)
|
||||
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
|
||||
|
||||
def _get_database_url(self):
|
||||
"""Get database URL from environment variables or configuration."""
|
||||
db_type = os.getenv('DATABASE_TYPE', 'sqlite').lower()
|
||||
|
||||
if db_type == 'postgresql':
|
||||
return self._get_postgresql_url()
|
||||
elif db_type in ['mysql', 'mariadb']:
|
||||
return self._get_mysql_url()
|
||||
elif db_type == 'sqlite':
|
||||
return self._get_sqlite_url()
|
||||
else:
|
||||
raise ValueError(f"Unsupported database type: {db_type}")
|
||||
|
||||
def _get_postgresql_url(self):
|
||||
"""Get PostgreSQL connection URL."""
|
||||
host = os.getenv('POSTGRES_HOST', 'localhost')
|
||||
port = os.getenv('POSTGRES_PORT', '5432')
|
||||
database = os.getenv('POSTGRES_DATABASE', 'hockey_results')
|
||||
username = os.getenv('POSTGRES_USER', 'postgres')
|
||||
password = os.getenv('POSTGRES_PASSWORD', '')
|
||||
|
||||
return f"postgresql://{username}:{password}@{host}:{port}/{database}"
|
||||
|
||||
def _get_mysql_url(self):
|
||||
"""Get MySQL/MariaDB connection URL."""
|
||||
host = os.getenv('MYSQL_HOST', 'localhost')
|
||||
port = os.getenv('MYSQL_PORT', '3306')
|
||||
database = os.getenv('MYSQL_DATABASE', 'hockey_results')
|
||||
username = os.getenv('MYSQL_USER', 'root')
|
||||
password = os.getenv('MYSQL_PASSWORD', '')
|
||||
charset = os.getenv('MYSQL_CHARSET', 'utf8mb4')
|
||||
|
||||
return f"mysql+pymysql://{username}:{password}@{host}:{port}/{database}?charset={charset}"
|
||||
|
||||
def _get_sqlite_url(self):
|
||||
"""Get SQLite connection URL."""
|
||||
database_path = os.getenv('SQLITE_DATABASE_PATH', 'hockey_results.db')
|
||||
return f"sqlite:///{database_path}"
|
||||
|
||||
def get_session(self):
|
||||
"""Get database session."""
|
||||
return self.SessionLocal()
|
||||
|
||||
def create_tables(self):
|
||||
"""Create all database tables."""
|
||||
Base.metadata.create_all(bind=self.engine)
|
||||
|
||||
# Global database configuration instance
|
||||
db_config = DatabaseConfig()
|
||||
|
||||
# Database Models
|
||||
class Player(Base):
|
||||
"""Player model."""
|
||||
__tablename__ = 'players'
|
||||
|
||||
player_number = Column(Integer, primary_key=True)
|
||||
player_forenames = Column(String(50))
|
||||
player_surname = Column(String(30))
|
||||
player_nickname = Column(String(30))
|
||||
player_chinese_name = Column(String(10))
|
||||
player_email = Column(String(255))
|
||||
player_dob = Column(Date)
|
||||
player_hkid = Column(String(20))
|
||||
player_tel_number = Column(String(30))
|
||||
player_team = Column(String(6))
|
||||
player_picture_url = Column(String(255))
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
class Club(Base):
|
||||
"""Club model."""
|
||||
__tablename__ = 'clubs'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
hockey_club = Column(String(100), unique=True, nullable=False)
|
||||
logo_url = Column(String(255))
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
class Team(Base):
|
||||
"""Team model."""
|
||||
__tablename__ = 'teams'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
club = Column(String(100), ForeignKey('clubs.hockey_club'))
|
||||
team = Column(String(10))
|
||||
display_name = Column(String(100))
|
||||
league = Column(String(50))
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
class MatchSquad(Base):
|
||||
"""Match squad model."""
|
||||
__tablename__ = 'match_squad'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
player_number = Column(Integer, ForeignKey('players.player_number'))
|
||||
player_forenames = Column(String(50))
|
||||
player_surname = Column(String(30))
|
||||
player_nickname = Column(String(30))
|
||||
match_date = Column(Date)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
class HockeyFixture(Base):
|
||||
"""Hockey fixture model."""
|
||||
__tablename__ = 'hockey_fixtures'
|
||||
|
||||
fixture_number = Column(Integer, primary_key=True)
|
||||
date = Column(Date)
|
||||
time = Column(String(10))
|
||||
home_team = Column(String(100))
|
||||
away_team = Column(String(100))
|
||||
venue = Column(String(255))
|
||||
home_score = Column(Integer)
|
||||
away_score = Column(Integer)
|
||||
umpire1 = Column(String(100))
|
||||
umpire2 = Column(String(100))
|
||||
match_official = Column(String(100))
|
||||
division = Column(String(50))
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
class AdminSettings(Base):
|
||||
"""Admin settings model."""
|
||||
__tablename__ = 'admin_settings'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
userid = Column(String(50), default='admin')
|
||||
next_fixture = Column(Integer)
|
||||
next_club = Column(String(100))
|
||||
next_team = Column(String(100))
|
||||
next_date = Column(Date)
|
||||
curr_motm = Column(Integer, ForeignKey('players.player_number'))
|
||||
curr_dotd = Column(Integer, ForeignKey('players.player_number'))
|
||||
oppo_logo = Column(String(255))
|
||||
hkfc_logo = Column(String(255))
|
||||
motm_url_suffix = Column(String(50))
|
||||
prev_fixture = Column(Integer)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
class MotmVote(Base):
|
||||
"""MOTM/DotD voting model."""
|
||||
__tablename__ = 'motm_votes'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
player_number = Column(Integer, ForeignKey('players.player_number'))
|
||||
player_name = Column(String(100))
|
||||
motm_total = Column(Integer, default=0)
|
||||
dotd_total = Column(Integer, default=0)
|
||||
goals_total = Column(Integer, default=0)
|
||||
assists_total = Column(Integer, default=0)
|
||||
fixture_number = Column(Integer)
|
||||
motm_votes = Column(Integer, default=0)
|
||||
dotd_votes = Column(Integer, default=0)
|
||||
goals = Column(Integer, default=0)
|
||||
assists = Column(Integer, default=0)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
class MatchComment(Base):
|
||||
"""Match comments model."""
|
||||
__tablename__ = 'match_comments'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
match_date = Column(Date)
|
||||
opposition = Column(String(100))
|
||||
comment = Column(Text)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
class HockeyUser(Base):
|
||||
"""User authentication model."""
|
||||
__tablename__ = 'hockey_users'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
username = Column(String(50), unique=True, nullable=False)
|
||||
email = Column(String(255), unique=True, nullable=False)
|
||||
password = Column(String(255), nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
last_login = Column(DateTime)
|
||||
|
||||
# Database utility functions
|
||||
def get_db_session():
|
||||
"""Get database session."""
|
||||
return db_config.get_session()
|
||||
|
||||
def execute_sql(sql_command, params=None):
|
||||
"""Execute SQL command with parameters."""
|
||||
session = get_db_session()
|
||||
try:
|
||||
if params:
|
||||
result = session.execute(sql_command, params)
|
||||
else:
|
||||
result = session.execute(sql_command)
|
||||
session.commit()
|
||||
return result
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
print(f"SQL Error: {e}")
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def fetch_all(sql_command, params=None):
|
||||
"""Fetch all results from SQL query."""
|
||||
session = get_db_session()
|
||||
try:
|
||||
if params:
|
||||
result = session.execute(sql_command, params)
|
||||
else:
|
||||
result = session.execute(sql_command)
|
||||
return result.fetchall()
|
||||
except Exception as e:
|
||||
print(f"SQL Error: {e}")
|
||||
return []
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def fetch_one(sql_command, params=None):
|
||||
"""Fetch one result from SQL query."""
|
||||
session = get_db_session()
|
||||
try:
|
||||
if params:
|
||||
result = session.execute(sql_command, params)
|
||||
else:
|
||||
result = session.execute(sql_command)
|
||||
return result.fetchone()
|
||||
except Exception as e:
|
||||
print(f"SQL Error: {e}")
|
||||
return None
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# Legacy compatibility functions
|
||||
def sql_write(sql_cmd):
|
||||
"""Legacy compatibility function for sql_write."""
|
||||
try:
|
||||
execute_sql(sql_cmd)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Write Error: {e}")
|
||||
return False
|
||||
|
||||
def sql_write_static(sql_cmd):
|
||||
"""Legacy compatibility function for sql_write_static."""
|
||||
return sql_write(sql_cmd)
|
||||
|
||||
def sql_read(sql_cmd):
|
||||
"""Legacy compatibility function for sql_read."""
|
||||
try:
|
||||
result = fetch_all(sql_cmd)
|
||||
# Convert to list of dictionaries for compatibility
|
||||
return [dict(row) for row in result] if result else []
|
||||
except Exception as e:
|
||||
print(f"Read Error: {e}")
|
||||
return []
|
||||
|
||||
def sql_read_static(sql_cmd):
|
||||
"""Legacy compatibility function for sql_read_static."""
|
||||
return sql_read(sql_cmd)
|
||||
|
||||
# Initialize database tables
|
||||
def init_database():
|
||||
"""Initialize database tables."""
|
||||
try:
|
||||
db_config.create_tables()
|
||||
print("Database tables created successfully")
|
||||
except Exception as e:
|
||||
print(f"Database initialization error: {e}")
|
||||
raise
|
||||
@ -1,121 +1,116 @@
|
||||
# encoding=utf-8
|
||||
import pymysql
|
||||
"""
|
||||
Database configuration module with SQLAlchemy support.
|
||||
This module provides backward compatibility with the old PyMySQL-based functions
|
||||
while using SQLAlchemy for database operations.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
|
||||
# These environment variables are configured in app.yaml.
|
||||
CLOUDSQL_CONNECTION_NAME = "hk-hockey:asia-east2:hk-hockey-sql"
|
||||
LOCAL_DB_SERVER = "mariadb.db.svc.cluster.local"
|
||||
CLOUDSQL_USER = "root"
|
||||
CLOUDSQL_WRITE_USER = "hockeyWrite"
|
||||
CLOUDSQL_READ_USER = "hockeyRead"
|
||||
CLOUDSQL_PASSWORD = "P8P1YopMlwg8TxhE"
|
||||
CLOUDSQL_WRITE_PASSWORD = "1URYcxXXlQ6xOWgj"
|
||||
CLOUDSQL_READ_PASSWORD = "o4GWrbbkBKy3oR6u"
|
||||
CLOUDSQL_DATABASE = "20209_hockeyResults"
|
||||
LOCAL_DATABASE = "hockeyResults2021"
|
||||
CLOUDSQL_DATABASE_STATIC = "hockeyResults"
|
||||
CLOUDSQL_CHARSET = "utf8"
|
||||
import warnings
|
||||
from database import (
|
||||
db_config,
|
||||
sql_write,
|
||||
sql_write_static,
|
||||
sql_read,
|
||||
sql_read_static,
|
||||
get_db_session,
|
||||
execute_sql,
|
||||
fetch_all,
|
||||
fetch_one,
|
||||
init_database
|
||||
)
|
||||
|
||||
# Legacy constants for backward compatibility
|
||||
CLOUDSQL_CONNECTION_NAME = os.getenv('CLOUDSQL_CONNECTION_NAME', "hk-hockey:asia-east2:hk-hockey-sql")
|
||||
LOCAL_DB_SERVER = os.getenv('LOCAL_DB_SERVER', "mariadb.db.svc.cluster.local")
|
||||
CLOUDSQL_USER = os.getenv('CLOUDSQL_USER', "root")
|
||||
CLOUDSQL_WRITE_USER = os.getenv('CLOUDSQL_WRITE_USER', "hockeyWrite")
|
||||
CLOUDSQL_READ_USER = os.getenv('CLOUDSQL_READ_USER', "hockeyRead")
|
||||
CLOUDSQL_PASSWORD = os.getenv('CLOUDSQL_PASSWORD', "P8P1YopMlwg8TxhE")
|
||||
CLOUDSQL_WRITE_PASSWORD = os.getenv('CLOUDSQL_WRITE_PASSWORD', "1URYcxXXlQ6xOWgj")
|
||||
CLOUDSQL_READ_PASSWORD = os.getenv('CLOUDSQL_READ_PASSWORD', "o4GWrbbkBKy3oR6u")
|
||||
CLOUDSQL_DATABASE = os.getenv('CLOUDSQL_DATABASE', "20209_hockeyResults")
|
||||
LOCAL_DATABASE = os.getenv('LOCAL_DATABASE', "hockeyResults2021")
|
||||
CLOUDSQL_DATABASE_STATIC = os.getenv('CLOUDSQL_DATABASE_STATIC', "hockeyResults")
|
||||
CLOUDSQL_CHARSET = os.getenv('CLOUDSQL_CHARSET', "utf8")
|
||||
|
||||
# Legacy functions for backward compatibility
|
||||
def write_cloudsql():
|
||||
# When deployed to App Engine, the `SERVER_SOFTWARE` environment variable
|
||||
# will be set to 'Google App Engine/version'.
|
||||
if os.getenv('SERVER_SOFTWARE', '').startswith('Google App Engine/'):
|
||||
# Connect using the unix socket located at
|
||||
# /cloudsql/cloudsql-connection-name.
|
||||
cloudsql_unix_socket = os.path.join('/cloudsql', CLOUDSQL_CONNECTION_NAME)
|
||||
db = pymysql.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=CLOUDSQL_DATABASE, charset=CLOUDSQL_CHARSET)
|
||||
else:
|
||||
db = pymysql.connect(host=LOCAL_DB_SERVER, user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=LOCAL_DATABASE, charset=CLOUDSQL_CHARSET)
|
||||
return db
|
||||
"""
|
||||
Legacy function - now uses SQLAlchemy.
|
||||
Returns a session object for compatibility.
|
||||
"""
|
||||
warnings.warn(
|
||||
"write_cloudsql() is deprecated. Use get_db_session() instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
return get_db_session()
|
||||
|
||||
def write_cloudsql_static():
|
||||
# When deployed to App Engine, the `SERVER_SOFTWARE` environment variable
|
||||
# will be set to 'Google App Engine/version'.
|
||||
if os.getenv('SERVER_SOFTWARE', '').startswith('Google App Engine/'):
|
||||
# Connect using the unix socket located at
|
||||
# /cloudsql/cloudsql-connection-name.
|
||||
cloudsql_unix_socket = os.path.join('/cloudsql', CLOUDSQL_CONNECTION_NAME)
|
||||
db = pymysql.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
|
||||
else:
|
||||
db = pymysql.connect(host=LOCAL_DB_SERVER, user=CLOUDSQL_WRITE_USER, passwd=CLOUDSQL_WRITE_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
|
||||
return db
|
||||
"""
|
||||
Legacy function - now uses SQLAlchemy.
|
||||
Returns a session object for compatibility.
|
||||
"""
|
||||
warnings.warn(
|
||||
"write_cloudsql_static() is deprecated. Use get_db_session() instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
return get_db_session()
|
||||
|
||||
def read_cloudsql():
|
||||
# When deployed to App Engine, the `SERVER_SOFTWARE` environment variable
|
||||
# will be set to 'Google App Engine/version'.
|
||||
if os.getenv('SERVER_SOFTWARE', '').startswith('Google App Engine/'):
|
||||
# Connect using the unix socket located at
|
||||
# /cloudsql/cloudsql-connection-name.
|
||||
cloudsql_unix_socket = os.path.join('/cloudsql', CLOUDSQL_CONNECTION_NAME)
|
||||
db = pymysql.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=CLOUDSQL_DATABASE, charset=CLOUDSQL_CHARSET)
|
||||
else:
|
||||
db = pymysql.connect(host=LOCAL_DB_SERVER, user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=LOCAL_DATABASE, charset=CLOUDSQL_CHARSET)
|
||||
return db
|
||||
"""
|
||||
Legacy function - now uses SQLAlchemy.
|
||||
Returns a session object for compatibility.
|
||||
"""
|
||||
warnings.warn(
|
||||
"read_cloudsql() is deprecated. Use get_db_session() instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
return get_db_session()
|
||||
|
||||
def read_cloudsql_static():
|
||||
# When deployed to App Engine, the `SERVER_SOFTWARE` environment variable
|
||||
# will be set to 'Google App Engine/version'.
|
||||
if os.getenv('SERVER_SOFTWARE', '').startswith('Google App Engine/'):
|
||||
# Connect using the unix socket located at
|
||||
# /cloudsql/cloudsql-connection-name.
|
||||
cloudsql_unix_socket = os.path.join('/cloudsql', CLOUDSQL_CONNECTION_NAME)
|
||||
db = pymysql.connect(unix_socket=cloudsql_unix_socket, user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
|
||||
else:
|
||||
db = pymysql.connect(host=LOCAL_DB_SERVER, user=CLOUDSQL_READ_USER, passwd=CLOUDSQL_READ_PASSWORD, db=CLOUDSQL_DATABASE_STATIC, charset=CLOUDSQL_CHARSET)
|
||||
return db
|
||||
"""
|
||||
Legacy function - now uses SQLAlchemy.
|
||||
Returns a session object for compatibility.
|
||||
"""
|
||||
warnings.warn(
|
||||
"read_cloudsql_static() is deprecated. Use get_db_session() instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
return get_db_session()
|
||||
|
||||
def sql_write(sql_cmd):
|
||||
try:
|
||||
db = write_cloudsql()
|
||||
cursor = db.cursor(pymysql.cursors.DictCursor)
|
||||
cursor.execute(sql_cmd)
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
finally:
|
||||
cursor.close()
|
||||
db.close()
|
||||
return db
|
||||
|
||||
def sql_write_static(sql_cmd):
|
||||
try:
|
||||
db = write_cloudsql_static()
|
||||
cursor = db.cursor(pymysql.cursors.DictCursor)
|
||||
cursor.execute(sql_cmd)
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
finally:
|
||||
cursor.close()
|
||||
db.close()
|
||||
return db
|
||||
|
||||
def sql_read(sql_cmd):
|
||||
try:
|
||||
db = read_cloudsql()
|
||||
cursor = db.cursor(pymysql.cursors.DictCursor)
|
||||
cursor.execute(sql_cmd)
|
||||
rows = cursor.fetchall()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
rows = ''
|
||||
finally:
|
||||
cursor.close()
|
||||
db.close()
|
||||
return rows
|
||||
|
||||
def sql_read_static(sql_cmd):
|
||||
try:
|
||||
db = read_cloudsql_static()
|
||||
cursor = db.cursor(pymysql.cursors.DictCursor)
|
||||
cursor.execute(sql_cmd)
|
||||
rows = cursor.fetchall()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
rows = ''
|
||||
finally:
|
||||
cursor.close()
|
||||
db.close()
|
||||
return rows
|
||||
# These functions now use SQLAlchemy but maintain the same interface
|
||||
__all__ = [
|
||||
'sql_write',
|
||||
'sql_write_static',
|
||||
'sql_read',
|
||||
'sql_read_static',
|
||||
'get_db_session',
|
||||
'execute_sql',
|
||||
'fetch_all',
|
||||
'fetch_one',
|
||||
'init_database',
|
||||
'db_config',
|
||||
# Legacy constants
|
||||
'CLOUDSQL_CONNECTION_NAME',
|
||||
'LOCAL_DB_SERVER',
|
||||
'CLOUDSQL_USER',
|
||||
'CLOUDSQL_WRITE_USER',
|
||||
'CLOUDSQL_READ_USER',
|
||||
'CLOUDSQL_PASSWORD',
|
||||
'CLOUDSQL_WRITE_PASSWORD',
|
||||
'CLOUDSQL_READ_PASSWORD',
|
||||
'CLOUDSQL_DATABASE',
|
||||
'LOCAL_DATABASE',
|
||||
'CLOUDSQL_DATABASE_STATIC',
|
||||
'CLOUDSQL_CHARSET',
|
||||
# Legacy functions
|
||||
'write_cloudsql',
|
||||
'write_cloudsql_static',
|
||||
'read_cloudsql',
|
||||
'read_cloudsql_static'
|
||||
]
|
||||
@ -1,7 +1,6 @@
|
||||
Flask>=2.0.0,<3.0.0
|
||||
Werkzeug>=2.0.0
|
||||
email-validator
|
||||
flask-mysql
|
||||
flask_login
|
||||
Flask-BasicAuth
|
||||
Flask-Bootstrap
|
||||
@ -9,4 +8,16 @@ flask_wtf
|
||||
wtforms>=3.0.0
|
||||
wtforms_components
|
||||
MarkupSafe>=2.0.0
|
||||
pymysql
|
||||
|
||||
# SQLAlchemy and database drivers
|
||||
SQLAlchemy>=2.0.0
|
||||
Flask-SQLAlchemy>=3.0.0
|
||||
alembic>=1.12.0
|
||||
|
||||
# Database drivers
|
||||
pymysql>=1.1.0
|
||||
psycopg2-binary>=2.9.0
|
||||
PyMySQL>=1.1.0
|
||||
|
||||
# Legacy support (can be removed after migration)
|
||||
flask-mysql
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
Flask>=2.0.0,<3.0.0
|
||||
Werkzeug>=2.0.0
|
||||
email-validator
|
||||
flask-mysql
|
||||
flask_login
|
||||
Flask-BasicAuth
|
||||
Flask-Bootstrap
|
||||
@ -10,3 +9,16 @@ wtforms>=3.0.0
|
||||
wtforms_components
|
||||
MarkupSafe>=2.0.0
|
||||
|
||||
# SQLAlchemy and database drivers
|
||||
SQLAlchemy>=2.0.0
|
||||
Flask-SQLAlchemy>=3.0.0
|
||||
alembic>=1.12.0
|
||||
|
||||
# Database drivers
|
||||
pymysql>=1.1.0
|
||||
psycopg2-binary>=2.9.0
|
||||
PyMySQL>=1.1.0
|
||||
|
||||
# Legacy support (can be removed after migration)
|
||||
flask-mysql
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user