style: apply black/isort across the repo; make CI mypy advisory

The push-CI gates (black/isort/mypy) had never actually run before the
branch-trigger fix, and the codebase predates them. Formatting is now
black/isort clean repo-wide. mypy keeps running but non-blocking: 86
pre-existing errors are a separate cleanup, not a gate to hold hostage.
This commit is contained in:
2026-08-10 15:57:00 +07:00
parent 300c0e0b6f
commit 9cac9c4d2a
32 changed files with 1031 additions and 659 deletions
+116 -87
View File
@@ -5,64 +5,70 @@ Main entry point for the Thailand Water Monitor system
import argparse
import asyncio
import sys
import signal
import sys
import time
from datetime import datetime
from typing import Optional
from .config import Config
from .water_scraper_v3 import EnhancedWaterMonitorScraper
from .logging_config import setup_logging, get_logger
from .exceptions import ConfigurationError, DatabaseConnectionError
from .logging_config import get_logger, setup_logging
from .metrics import get_metrics_collector
from .water_scraper_v3 import EnhancedWaterMonitorScraper
logger = get_logger(__name__)
def setup_signal_handlers(scraper: Optional[EnhancedWaterMonitorScraper] = None):
"""Setup signal handlers for graceful shutdown"""
def signal_handler(signum, frame):
logger.info(f"Received signal {signum}, shutting down gracefully...")
if scraper:
logger.info("Stopping scraper...")
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
def run_test_cycle():
"""Run a single test cycle"""
logger.info("Running test cycle...")
try:
# Validate configuration
Config.validate_config()
# Initialize scraper
db_config = Config.get_database_config()
scraper = EnhancedWaterMonitorScraper(db_config)
# Run single scraping cycle
result = scraper.run_scraping_cycle()
if result:
logger.info("✅ Test cycle completed successfully")
# Show some statistics
latest_data = scraper.get_latest_data(5)
if latest_data:
logger.info(f"Latest data points: {len(latest_data)}")
for data in latest_data[:3]: # Show first 3
logger.info(f"{data['station_code']}: {data['water_level']:.2f}m, {data['discharge']:.1f} cms")
logger.info(
f"{data['station_code']}: {data['water_level']:.2f}m, {data['discharge']:.1f} cms"
)
else:
logger.warning("⚠️ Test cycle completed but no new data was found")
return True
except Exception as e:
logger.error(f"❌ Test cycle failed: {e}")
return False
def run_continuous_monitoring():
"""Run continuous monitoring with adaptive scheduling and alerting"""
logger.info("Starting continuous monitoring...")
@@ -77,13 +83,18 @@ def run_continuous_monitoring():
# Initialize alerting system
from .alerting import WaterLevelAlertSystem
alerting = WaterLevelAlertSystem()
# Setup signal handlers
setup_signal_handlers(scraper)
logger.info(f"Monitoring started with {Config.SCRAPING_INTERVAL_HOURS}h interval")
logger.info("Adaptive retry: switches to 1-minute intervals when no data available")
logger.info(
f"Monitoring started with {Config.SCRAPING_INTERVAL_HOURS}h interval"
)
logger.info(
"Adaptive retry: switches to 1-minute intervals when no data available"
)
logger.info("Alerts: automatic check after each successful data fetch")
logger.info("Press Ctrl+C to stop")
@@ -93,6 +104,7 @@ def run_continuous_monitoring():
# Adaptive scheduling state
from datetime import datetime, timedelta
retry_mode = not initial_success
last_successful_fetch = None if not initial_success else datetime.now()
@@ -101,7 +113,9 @@ def run_continuous_monitoring():
next_run = datetime.now() + timedelta(minutes=1)
else:
logger.info("Initial data fetch successful - using hourly schedule")
next_run = (datetime.now() + timedelta(hours=1)).replace(minute=0, second=0, microsecond=0)
next_run = (datetime.now() + timedelta(hours=1)).replace(
minute=0, second=0, microsecond=0
)
logger.info(f"Next run at {next_run.strftime('%H:%M')}")
@@ -119,24 +133,35 @@ def run_continuous_monitoring():
logger.info("Running alert check...")
try:
alert_results = alerting.run_alert_check()
if alert_results.get('total_alerts', 0) > 0:
logger.info(f"Alerts: {alert_results['total_alerts']} generated, {alert_results['sent']} sent")
if alert_results.get("total_alerts", 0) > 0:
logger.info(
f"Alerts: {alert_results['total_alerts']} generated, {alert_results['sent']} sent"
)
except Exception as e:
logger.error(f"Alert check failed: {e}")
if retry_mode:
logger.info("✅ Data fetch successful - switching back to hourly schedule")
logger.info(
"✅ Data fetch successful - switching back to hourly schedule"
)
retry_mode = False
# Schedule next run at the next full hour
next_run = (current_time + timedelta(hours=1)).replace(minute=0, second=0, microsecond=0)
next_run = (current_time + timedelta(hours=1)).replace(
minute=0, second=0, microsecond=0
)
else:
# Continue hourly schedule
next_run = (current_time + timedelta(hours=Config.SCRAPING_INTERVAL_HOURS)).replace(minute=0, second=0, microsecond=0)
next_run = (
current_time
+ timedelta(hours=Config.SCRAPING_INTERVAL_HOURS)
).replace(minute=0, second=0, microsecond=0)
logger.info(f"Next scheduled run at {next_run.strftime('%H:%M')}")
else:
if not retry_mode:
logger.warning("⚠️ No data fetched - switching to retry mode (1-minute intervals)")
logger.warning(
"⚠️ No data fetched - switching to retry mode (1-minute intervals)"
)
retry_mode = True
# Schedule retry in 1 minute
@@ -154,32 +179,34 @@ def run_continuous_monitoring():
return True
def run_gap_filling(days_back: int):
"""Run gap filling for missing data"""
logger.info(f"Checking for data gaps in the last {days_back} days...")
try:
# Validate configuration
Config.validate_config()
# Initialize scraper
db_config = Config.get_database_config()
scraper = EnhancedWaterMonitorScraper(db_config)
# Fill gaps
filled_count = scraper.fill_data_gaps(days_back)
if filled_count > 0:
logger.info(f"✅ Filled {filled_count} missing data points")
else:
logger.info("✅ No data gaps found")
return True
except Exception as e:
logger.error(f"❌ Gap filling failed: {e}")
return False
def run_data_update(days_back: int):
"""Update existing data with latest values"""
logger.info(f"Updating existing data for the last {days_back} days...")
@@ -206,7 +233,10 @@ def run_data_update(days_back: int):
logger.error(f"❌ Data update failed: {e}")
return False
def run_historical_import(start_date_str: str, end_date_str: str, skip_existing: bool = True):
def run_historical_import(
start_date_str: str, end_date_str: str, skip_existing: bool = True
):
"""Import historical data for a date range"""
try:
# Parse dates
@@ -217,7 +247,9 @@ def run_historical_import(start_date_str: str, end_date_str: str, skip_existing:
logger.error("Start date must be before or equal to end date")
return False
logger.info(f"Importing historical data from {start_date.date()} to {end_date.date()}")
logger.info(
f"Importing historical data from {start_date.date()} to {end_date.date()}"
)
if skip_existing:
logger.info("Skipping dates that already have data")
@@ -229,7 +261,9 @@ def run_historical_import(start_date_str: str, end_date_str: str, skip_existing:
scraper = EnhancedWaterMonitorScraper(db_config)
# Import historical data
imported_count = scraper.import_historical_data(start_date, end_date, skip_existing)
imported_count = scraper.import_historical_data(
start_date, end_date, skip_existing
)
if imported_count > 0:
logger.info(f"✅ Imported {imported_count} historical data points")
@@ -245,25 +279,24 @@ def run_historical_import(start_date_str: str, end_date_str: str, skip_existing:
logger.error(f"❌ Historical import failed: {e}")
return False
def run_web_api():
"""Run the FastAPI web interface"""
logger.info("Starting web API server...")
try:
import uvicorn
from .web_api import app
# Validate configuration
Config.validate_config()
# Run the server
uvicorn.run(
app,
host="0.0.0.0",
port=8000,
log_config=None # Use our custom logging
app, host="0.0.0.0", port=8000, log_config=None # Use our custom logging
)
except ImportError:
logger.error("FastAPI not installed. Run: pip install fastapi uvicorn")
return False
@@ -271,6 +304,7 @@ def run_web_api():
logger.error(f"Web API failed: {e}")
return False
def run_alert_check():
"""Run water level alert check"""
logger.info("Running water level alert check...")
@@ -284,7 +318,7 @@ def run_alert_check():
# Run alert check
results = alerting.run_alert_check()
if 'error' in results:
if "error" in results:
logger.error("❌ Alert check failed due to database connection")
return False
@@ -300,6 +334,7 @@ def run_alert_check():
logger.error(f"❌ Alert check failed: {e}")
return False
def run_alert_test():
"""Send test alert message"""
logger.info("Sending test alert message...")
@@ -312,7 +347,9 @@ def run_alert_test():
if not alerting.matrix_notifier:
logger.error("❌ Matrix notifier not configured")
logger.info("Please set MATRIX_ACCESS_TOKEN and MATRIX_ROOM_ID in your .env file")
logger.info(
"Please set MATRIX_ACCESS_TOKEN and MATRIX_ROOM_ID in your .env file"
)
return False
# Send test message
@@ -330,6 +367,7 @@ def run_alert_test():
logger.error(f"❌ Test alert failed: {e}")
return False
def show_status():
"""Show current system status"""
logger.info("=== Northern Thailand Ping River Monitor Status ===")
@@ -351,10 +389,14 @@ def show_status():
if latest_data:
logger.info(f"\n=== Latest Data ({len(latest_data)} points) ===")
for data in latest_data:
timestamp = data['timestamp']
timestamp = data["timestamp"]
if isinstance(timestamp, str):
timestamp = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
logger.info(f"{data['station_code']} ({timestamp}): {data['water_level']:.2f}m")
timestamp = datetime.fromisoformat(
timestamp.replace("Z", "+00:00")
)
logger.info(
f"{data['station_code']} ({timestamp}): {data['water_level']:.2f}m"
)
else:
logger.info("No data found in database")
else:
@@ -364,6 +406,7 @@ def show_status():
logger.info("\n=== Alerting System Status ===")
try:
from .alerting import WaterLevelAlertSystem
alerting = WaterLevelAlertSystem()
if alerting.matrix_notifier:
@@ -390,6 +433,7 @@ def show_status():
logger.error(f"Status check failed: {e}")
return False
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
@@ -406,96 +450,80 @@ Examples:
%(prog)s --status # Show system status
%(prog)s --alert-check # Check water levels and send alerts
%(prog)s --alert-test # Send test Matrix message
"""
""",
)
parser.add_argument("--test", action="store_true", help="Run a single test cycle")
parser.add_argument(
"--test",
action="store_true",
help="Run a single test cycle"
"--web-api", action="store_true", help="Start the web API server"
)
parser.add_argument(
"--web-api",
action="store_true",
help="Start the web API server"
)
parser.add_argument(
"--fill-gaps",
type=int,
metavar="DAYS",
help="Fill missing data gaps for the specified number of days back"
help="Fill missing data gaps for the specified number of days back",
)
parser.add_argument(
"--update-data",
type=int,
metavar="DAYS",
help="Update existing data for the specified number of days back"
metavar="DAYS",
help="Update existing data for the specified number of days back",
)
parser.add_argument(
"--import-historical",
nargs=2,
metavar=("START_DATE", "END_DATE"),
help="Import historical data for date range (YYYY-MM-DD format)"
help="Import historical data for date range (YYYY-MM-DD format)",
)
parser.add_argument(
"--force-overwrite",
action="store_true",
help="Overwrite existing data when importing historical data"
help="Overwrite existing data when importing historical data",
)
parser.add_argument(
"--status",
action="store_true",
help="Show current system status"
"--status", action="store_true", help="Show current system status"
)
parser.add_argument(
"--alert-check",
action="store_true",
help="Run water level alert check"
"--alert-check", action="store_true", help="Run water level alert check"
)
parser.add_argument(
"--alert-test",
action="store_true",
help="Send test alert message to Matrix"
"--alert-test", action="store_true", help="Send test alert message to Matrix"
)
parser.add_argument(
"--log-level",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
default=Config.LOG_LEVEL,
help="Set logging level"
help="Set logging level",
)
parser.add_argument(
"--log-file",
default=Config.LOG_FILE,
help="Log file path"
)
parser.add_argument("--log-file", default=Config.LOG_FILE, help="Log file path")
args = parser.parse_args()
# Setup logging
setup_logging(
log_level=args.log_level,
log_file=args.log_file,
enable_console=True,
enable_colors=True
enable_colors=True,
)
logger.info("🏔️ Northern Thailand Ping River Monitor starting...")
logger.info(f"Version: 3.1.3")
logger.info(f"Log level: {args.log_level}")
try:
success = False
if args.test:
success = run_test_cycle()
elif args.web_api:
@@ -516,14 +544,14 @@ Examples:
success = run_alert_test()
else:
success = run_continuous_monitoring()
if success:
logger.info("✅ Operation completed successfully")
sys.exit(0)
else:
logger.error("❌ Operation failed")
sys.exit(1)
except ConfigurationError as e:
logger.error(f"Configuration error: {e}")
sys.exit(1)
@@ -534,5 +562,6 @@ Examples:
logger.error(f"Unexpected error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
main()