- **Migration to uv package manager**: Replace pip/requirements with modern pyproject.toml - Add pyproject.toml with complete dependency management - Update all scripts and Makefile to use uv commands - Maintain backward compatibility with existing workflows - **PostgreSQL integration and migration tools**: - Enhanced config.py with automatic password URL encoding - Complete PostgreSQL setup scripts and documentation - High-performance SQLite to PostgreSQL migration tool (91x speed improvement) - Support for both connection strings and individual components - **Executable distribution system**: - PyInstaller integration for standalone .exe creation - Automated build scripts with batch file generation - Complete packaging system for end-user distribution - **Enhanced data management**: - Fix --fill-gaps command with proper method implementation - Add gap detection and historical data backfill capabilities - Implement data update functionality for existing records - Add comprehensive database adapter methods - **Developer experience improvements**: - Password encoding tools for special characters - Interactive setup wizards for PostgreSQL configuration - Comprehensive documentation and migration guides - Automated testing and validation tools 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
175 lines
6.0 KiB
Python
175 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
PostgreSQL setup script for Northern Thailand Ping River Monitor
|
|
This script helps you configure and test your PostgreSQL connection
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import logging
|
|
from typing import Optional
|
|
from urllib.parse import urlparse
|
|
|
|
def setup_logging():
|
|
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
|
|
|
def test_postgres_connection(connection_string: str) -> bool:
|
|
"""Test connection to PostgreSQL database"""
|
|
try:
|
|
from sqlalchemy import create_engine, text
|
|
|
|
# Test connection
|
|
engine = create_engine(connection_string, pool_pre_ping=True)
|
|
with engine.connect() as conn:
|
|
result = conn.execute(text("SELECT version()"))
|
|
version = result.fetchone()[0]
|
|
logging.info(f"✅ Connected to PostgreSQL successfully!")
|
|
logging.info(f"Database version: {version}")
|
|
return True
|
|
|
|
except ImportError:
|
|
logging.error("❌ psycopg2-binary not installed. Run: uv add psycopg2-binary")
|
|
return False
|
|
except Exception as e:
|
|
logging.error(f"❌ Connection failed: {e}")
|
|
return False
|
|
|
|
def parse_connection_string(connection_string: str) -> dict:
|
|
"""Parse PostgreSQL connection string into components"""
|
|
try:
|
|
parsed = urlparse(connection_string)
|
|
return {
|
|
'host': parsed.hostname,
|
|
'port': parsed.port or 5432,
|
|
'database': parsed.path[1:] if parsed.path else None,
|
|
'username': parsed.username,
|
|
'password': parsed.password,
|
|
}
|
|
except Exception as e:
|
|
logging.error(f"Failed to parse connection string: {e}")
|
|
return {}
|
|
|
|
def create_database_if_not_exists(connection_string: str, database_name: str) -> bool:
|
|
"""Create database if it doesn't exist"""
|
|
try:
|
|
from sqlalchemy import create_engine, text
|
|
|
|
# Connect to default postgres database to create our database
|
|
parsed = urlparse(connection_string)
|
|
admin_connection = connection_string.replace(f"/{parsed.path[1:]}", "/postgres")
|
|
|
|
engine = create_engine(admin_connection, pool_pre_ping=True)
|
|
|
|
with engine.connect() as conn:
|
|
# Check if database exists
|
|
result = conn.execute(text(
|
|
"SELECT 1 FROM pg_database WHERE datname = :db_name"
|
|
), {"db_name": database_name})
|
|
|
|
if result.fetchone():
|
|
logging.info(f"✅ Database '{database_name}' already exists")
|
|
return True
|
|
else:
|
|
# Create database
|
|
conn.execute(text("COMMIT")) # End transaction
|
|
conn.execute(text(f'CREATE DATABASE "{database_name}"'))
|
|
logging.info(f"✅ Created database '{database_name}'")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logging.error(f"❌ Failed to create database: {e}")
|
|
return False
|
|
|
|
def initialize_tables(connection_string: str) -> bool:
|
|
"""Initialize database tables"""
|
|
try:
|
|
# Import the database adapter to create tables
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
from database_adapters import SQLAdapter
|
|
|
|
adapter = SQLAdapter(connection_string=connection_string, db_type='postgresql')
|
|
if adapter.connect():
|
|
logging.info("✅ Database tables initialized successfully")
|
|
return True
|
|
else:
|
|
logging.error("❌ Failed to initialize tables")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logging.error(f"❌ Failed to initialize tables: {e}")
|
|
return False
|
|
|
|
def interactive_setup():
|
|
"""Interactive setup wizard"""
|
|
print("🐘 PostgreSQL Setup Wizard for Ping River Monitor")
|
|
print("=" * 50)
|
|
|
|
# Get connection details
|
|
host = input("PostgreSQL host (e.g., 192.168.1.100): ").strip()
|
|
port = input("PostgreSQL port [5432]: ").strip() or "5432"
|
|
database = input("Database name [water_monitoring]: ").strip() or "water_monitoring"
|
|
username = input("Username: ").strip()
|
|
password = input("Password: ").strip()
|
|
|
|
# Optional SSL
|
|
use_ssl = input("Use SSL connection? (y/N): ").strip().lower() == 'y'
|
|
ssl_params = "?sslmode=require" if use_ssl else ""
|
|
|
|
connection_string = f"postgresql://{username}:{password}@{host}:{port}/{database}{ssl_params}"
|
|
|
|
print(f"\nGenerated connection string:")
|
|
print(f"POSTGRES_CONNECTION_STRING={connection_string}")
|
|
|
|
return connection_string
|
|
|
|
def main():
|
|
setup_logging()
|
|
|
|
print("🚀 Northern Thailand Ping River Monitor - PostgreSQL Setup")
|
|
print("=" * 60)
|
|
|
|
# Check if connection string is provided via environment
|
|
connection_string = os.getenv('POSTGRES_CONNECTION_STRING')
|
|
|
|
if not connection_string:
|
|
print("No POSTGRES_CONNECTION_STRING found in environment.")
|
|
print("Starting interactive setup...\n")
|
|
connection_string = interactive_setup()
|
|
|
|
# Suggest adding to .env file
|
|
print(f"\n💡 Add this to your .env file:")
|
|
print(f"DB_TYPE=postgresql")
|
|
print(f"POSTGRES_CONNECTION_STRING={connection_string}")
|
|
|
|
# Parse connection details
|
|
config = parse_connection_string(connection_string)
|
|
if not config.get('host'):
|
|
logging.error("Invalid connection string format")
|
|
return False
|
|
|
|
print(f"\n🔗 Connecting to PostgreSQL at {config['host']}:{config['port']}")
|
|
|
|
# Test connection
|
|
if not test_postgres_connection(connection_string):
|
|
return False
|
|
|
|
# Try to create database
|
|
database_name = config.get('database', 'water_monitoring')
|
|
if database_name:
|
|
create_database_if_not_exists(connection_string, database_name)
|
|
|
|
# Initialize tables
|
|
if not initialize_tables(connection_string):
|
|
return False
|
|
|
|
print("\n🎉 PostgreSQL setup completed successfully!")
|
|
print("\nNext steps:")
|
|
print("1. Update your .env file with the connection string")
|
|
print("2. Run: make run-test")
|
|
print("3. Run: make run-api")
|
|
|
|
return True
|
|
|
|
if __name__ == "__main__":
|
|
success = main()
|
|
sys.exit(0 if success else 1) |