Implemented the full clean rewrite for Gitea
Test blackout notifier / test (push) Successful in 3m41s
Hourly blackout check / check (push) Successful in 8s

This commit is contained in:
Meghdad
2026-07-17 04:33:16 +03:30
parent d7f53ba234
commit 4218e4ac36
30 changed files with 1437 additions and 384 deletions
+165
View File
@@ -0,0 +1,165 @@
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Any
from blackout_notifier.clients import ExternalServiceError, Notifier, SaapaClient
from blackout_notifier.messages import (
cancelled_outage_message,
new_outage_message,
reactivated_outage_message,
updated_outage_message,
)
from blackout_notifier.models import Outage, OutageDataError, jalali_query_window
from blackout_notifier.state import StateRepository
LOGGER = logging.getLogger(__name__)
@dataclass(slots=True)
class RunResult:
sent: int = 0
state_changed: bool = False
errors: list[str] = field(default_factory=list)
class OutageService:
def __init__(
self,
*,
bill_ids: tuple[str, ...],
lookahead_days: int,
saapa: SaapaClient,
notifier: Notifier,
state_repository: StateRepository,
) -> None:
self._bill_ids = bill_ids
self._lookahead_days = lookahead_days
self._saapa = saapa
self._notifier = notifier
self._state_repository = state_repository
def run(self, now: datetime, *, persist: bool = True) -> RunResult:
state = self._state_repository.load()
result = RunResult()
from_date, to_date = jalali_query_window(now, self._lookahead_days)
for bill_id in self._bill_ids:
try:
outages = self._saapa.fetch(bill_id, from_date, to_date)
except ExternalServiceError as error:
result.errors.append(str(error))
LOGGER.error("%s", error)
continue
self._process_bill(state, bill_id, outages, now, result)
if self._prune_expired(state, now):
result.state_changed = True
if result.state_changed and persist:
state["updated_at"] = now.isoformat()
self._state_repository.save(state)
return result
def _process_bill(
self,
state: dict[str, Any],
bill_id: str,
outages: list[Outage],
now: datetime,
result: RunResult,
) -> None:
bills = state["bills"]
bill_state = bills.get(bill_id)
if bill_state is None:
bill_state = {"events": {}}
records: dict[str, dict[str, Any]] = bill_state["events"]
current = {
outage.outage_number: outage for outage in outages if outage.stop_datetime() > now
}
for event_id, outage in current.items():
record = records.get(event_id)
if record is None:
if self._notify(new_outage_message(bill_id, outage, now), result):
records[event_id] = _record(outage, now)
result.state_changed = True
continue
previous = Outage.from_dict(record["snapshot"])
if record["status"] == "cancelled":
if self._notify(reactivated_outage_message(bill_id, outage, now), result):
records[event_id] = _record(outage, now)
result.state_changed = True
continue
if outage.fingerprint() != record["notified_hash"]:
if self._notify(updated_outage_message(bill_id, previous, outage, now), result):
records[event_id] = _record(outage, now)
result.state_changed = True
continue
if record["missing_checks"]:
record["missing_checks"] = 0
record["last_seen_at"] = now.isoformat()
result.state_changed = True
for event_id, record in list(records.items()):
if event_id in current or record["status"] != "active":
continue
try:
previous = Outage.from_dict(record["snapshot"])
except OutageDataError as error:
result.errors.append(f"Invalid stored outage for bill {bill_id}: {error}")
continue
if previous.start_datetime() <= now:
continue
if record["missing_checks"] == 0:
record["missing_checks"] = 1
result.state_changed = True
elif self._notify(cancelled_outage_message(bill_id, previous, now), result):
record["status"] = "cancelled"
record["missing_checks"] = 2
record["notified_at"] = now.isoformat()
result.state_changed = True
if records and bill_id not in bills:
bills[bill_id] = bill_state
def _notify(self, message: str, result: RunResult) -> bool:
try:
self._notifier.send(message)
except ExternalServiceError as error:
result.errors.append(str(error))
LOGGER.error("%s", error)
return False
result.sent += 1
return True
@staticmethod
def _prune_expired(state: dict[str, Any], now: datetime) -> bool:
changed = False
cutoff = now - timedelta(days=30)
for bill_id, bill_state in list(state["bills"].items()):
records = bill_state["events"]
for event_id, record in list(records.items()):
outage = Outage.from_dict(record["snapshot"])
if outage.stop_datetime() < cutoff:
del records[event_id]
changed = True
if not records:
del state["bills"][bill_id]
changed = True
return changed
def _record(outage: Outage, now: datetime) -> dict[str, Any]:
return {
"snapshot": outage.to_dict(),
"notified_hash": outage.fingerprint(),
"status": "active",
"missing_checks": 0,
"last_seen_at": now.isoformat(),
"notified_at": now.isoformat(),
}