feat: --fill-gaps all repairs missing data across the whole DB range
Documentation / Generate API Documentation (push) Successful in 11s
Documentation / Build Sphinx Documentation (push) Successful in 17s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 1s
Documentation / Documentation Summary (push) Successful in 3s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 29s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 14s
Documentation / Validate Documentation (push) Failing after 9s
Documentation / Generate API Documentation (push) Successful in 11s
Documentation / Build Sphinx Documentation (push) Successful in 17s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 1s
Documentation / Documentation Summary (push) Successful in 3s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 29s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 14s
Documentation / Validate Documentation (push) Failing after 9s
Gap detection is now hour-granular: days with partial data (missing hourly slots) are re-fetched, not just days with no rows at all. The API's hour-24-is-next-midnight quirk is handled by re-fetching day D-1 when day D is missing its 00:00 slot. SQL adapters gain single-query range and recorded-hours lookups; non-SQL backends fall back to the old day-granular check.
This commit is contained in:
+106
-30
@@ -579,53 +579,129 @@ class EnhancedWaterMonitorScraper:
|
||||
increment_counter("scraping_cycles_failed")
|
||||
return False
|
||||
|
||||
def fill_data_gaps(self, days_back: int) -> int:
|
||||
"""Fill gaps in data for the specified number of days back"""
|
||||
def fill_data_gaps(self, days_back: Optional[int] = None) -> int:
|
||||
"""Fill gaps in stored data by re-fetching incomplete days.
|
||||
|
||||
Args:
|
||||
days_back: How many days back to scan. None scans the whole data
|
||||
range, from the earliest measurement in the database to now.
|
||||
|
||||
Detection is hour-granular where the backend supports it: any calendar
|
||||
day missing one or more hourly slots gets its source date(s) re-fetched.
|
||||
Because the API reports hour 24 as midnight of the next day, a missing
|
||||
00:00 slot on day D is repaired by re-fetching day D-1.
|
||||
"""
|
||||
logger = get_logger(__name__)
|
||||
filled_count = 0
|
||||
|
||||
try:
|
||||
# Calculate date range
|
||||
end_date = datetime.datetime.now()
|
||||
start_date = end_date - datetime.timedelta(days=days_back)
|
||||
now = datetime.datetime.now()
|
||||
end_date = now.date()
|
||||
|
||||
logger.info(
|
||||
f"Checking for gaps from {start_date.date()} to {end_date.date()}"
|
||||
)
|
||||
if days_back is not None:
|
||||
start_date = end_date - datetime.timedelta(days=days_back)
|
||||
else:
|
||||
if not self.db_adapter:
|
||||
logger.error("Database adapter not initialized")
|
||||
return 0
|
||||
date_range = self.db_adapter.get_measurement_date_range()
|
||||
if not date_range:
|
||||
logger.error(
|
||||
"Database is empty or does not support range queries; "
|
||||
"use --import-historical to seed data first"
|
||||
)
|
||||
return 0
|
||||
start_date = date_range[0].date()
|
||||
|
||||
# Iterate through each date in the range
|
||||
current_date = start_date
|
||||
while current_date <= end_date:
|
||||
# Check if we have data for this date
|
||||
has_data = self._check_data_exists_for_date(current_date)
|
||||
logger.info(f"Checking for gaps from {start_date} to {end_date}")
|
||||
|
||||
if not has_data:
|
||||
logger.info(f"Filling gap for date: {current_date.date()}")
|
||||
fetch_dates = self._find_gap_fetch_dates(start_date, end_date, now)
|
||||
|
||||
# Fetch data for this specific date
|
||||
data = self.fetch_water_data_for_date(current_date)
|
||||
if not fetch_dates:
|
||||
logger.info("No gaps found")
|
||||
return 0
|
||||
|
||||
if data:
|
||||
# Save the data
|
||||
if self.save_to_database(data):
|
||||
filled_count += len(data)
|
||||
logger.info(
|
||||
f"Filled {len(data)} measurements for {current_date.date()}"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Failed to save data for {current_date.date()}"
|
||||
)
|
||||
logger.info(f"Found {len(fetch_dates)} day(s) needing a re-fetch")
|
||||
|
||||
for fetch_date in sorted(fetch_dates):
|
||||
fetch_dt = datetime.datetime.combine(fetch_date, datetime.time.min)
|
||||
logger.info(f"Filling gap for date: {fetch_date}")
|
||||
|
||||
data = self.fetch_water_data_for_date(fetch_dt)
|
||||
|
||||
if data:
|
||||
if self.save_to_database(data):
|
||||
filled_count += len(data)
|
||||
logger.info(
|
||||
f"Filled {len(data)} measurements for {fetch_date}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"No data available for {current_date.date()}")
|
||||
logger.warning(f"Failed to save data for {fetch_date}")
|
||||
else:
|
||||
logger.warning(f"No data available for {fetch_date}")
|
||||
|
||||
current_date += datetime.timedelta(days=1)
|
||||
# Be respectful to the API
|
||||
time.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Gap filling error: {e}")
|
||||
|
||||
return filled_count
|
||||
|
||||
def _find_gap_fetch_dates(
|
||||
self,
|
||||
start_date: datetime.date,
|
||||
end_date: datetime.date,
|
||||
now: datetime.datetime,
|
||||
) -> List[datetime.date]:
|
||||
"""Determine which source dates need re-fetching to fill gaps.
|
||||
|
||||
Uses hour-granular detection when the adapter supports it, otherwise
|
||||
falls back to re-fetching only days with no data at all.
|
||||
"""
|
||||
hours_by_day = None
|
||||
if self.db_adapter:
|
||||
hours_by_day = self.db_adapter.get_recorded_hours_by_day(
|
||||
start_date, end_date
|
||||
)
|
||||
|
||||
fetch_dates = set()
|
||||
|
||||
if hours_by_day is None:
|
||||
logger.info(
|
||||
"Backend does not support hour-granular gap detection; "
|
||||
"checking for fully-missing days only"
|
||||
)
|
||||
current = start_date
|
||||
while current <= end_date:
|
||||
current_dt = datetime.datetime.combine(current, datetime.time.min)
|
||||
if not self._check_data_exists_for_date(current_dt):
|
||||
fetch_dates.add(current)
|
||||
current += datetime.timedelta(days=1)
|
||||
return sorted(fetch_dates)
|
||||
|
||||
current = start_date
|
||||
while current <= end_date:
|
||||
if current == now.date():
|
||||
# Today: only expect hours that have already passed
|
||||
expected_hours = set(range(0, now.hour))
|
||||
else:
|
||||
expected_hours = set(range(24))
|
||||
|
||||
missing = expected_hours - hours_by_day.get(current, set())
|
||||
|
||||
if missing:
|
||||
# Hours 1-23 of day D come from fetching D; hour 0 comes from
|
||||
# the previous day's fetch (the API's "hour 24")
|
||||
if any(h >= 1 for h in missing):
|
||||
fetch_dates.add(current)
|
||||
if 0 in missing and current > start_date:
|
||||
fetch_dates.add(current - datetime.timedelta(days=1))
|
||||
|
||||
current += datetime.timedelta(days=1)
|
||||
|
||||
return sorted(fetch_dates)
|
||||
|
||||
def update_existing_data(self, days_back: int) -> int:
|
||||
"""Update existing data with latest values for the specified number of days back"""
|
||||
logger = get_logger(__name__)
|
||||
|
||||
Reference in New Issue
Block a user