Implemented the full clean rewrite for Gitea
This commit is contained in:
@@ -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(),
|
||||
}
|
||||
Reference in New Issue
Block a user