Load config, harden processing and notifications
- Actually load configs/config.yaml at startup (init_config was commented out) - Add timeouts and error handling to all HTTP calls; run blocking requests in threads; serialize processing with a lock; run initial check in background - URL-encode Telegram messages and HTML-escape scraped data - Fix GPV crash on empty schedule, 00:00 end time, date sorting and fallback format - Parse PO table per row; fix daily reminder check and change detection - Use per-instance timestamps in dataclasses; robust config value parsing - Matrix: use v3 PUT send endpoint; strip secrets from logged errors - Resolve templates/static/test paths relative to the project; use lifespan - Move debug GPV data to test/data_gpv.json; add CLAUDE.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e3367c6a2e
commit
22fc8b38bf
@@ -1,15 +1,28 @@
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import List, Self
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional, Self
|
||||
|
||||
import app_config as cfg
|
||||
|
||||
LOG = cfg.LOG
|
||||
|
||||
PO_DATETIME_FORMAT = '%Y.%m.%d %H:%M'
|
||||
GPV_DATETIME_FORMAT = '%d.%m.%Y %H:%M'
|
||||
GPV_DATE_FORMAT = '%d.%m.%Y'
|
||||
|
||||
|
||||
def parse_ts(value: str, fmt: str) -> Optional[float]:
|
||||
try:
|
||||
return datetime.strptime(value, fmt).timestamp()
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class POData:
|
||||
city: str = field(repr=False)
|
||||
street: str = field(repr=False)
|
||||
house: int = field(repr=False)
|
||||
house: str = field(repr=False)
|
||||
po_type: str = field(repr=False)
|
||||
reason: str = field(repr=False)
|
||||
placement_date: str
|
||||
@@ -17,16 +30,23 @@ class POData:
|
||||
stop_date: str
|
||||
start_time: str
|
||||
stop_time: str
|
||||
notified_at: datetime = field(repr=False, default=None)
|
||||
updated_at: datetime = field(repr=False, default=datetime.now())
|
||||
need_to_send: bool = field(repr=False, default=True)
|
||||
start_ts: float = field(init=False)
|
||||
# service fields are not compared: equality means the same outage content
|
||||
notified_at: Optional[datetime] = field(repr=False, default=None, compare=False)
|
||||
updated_at: datetime = field(repr=False, default_factory=datetime.now, compare=False)
|
||||
need_to_send: bool = field(repr=False, default=True, compare=False)
|
||||
start_ts: float = field(init=False, default=0.0, compare=False)
|
||||
|
||||
def __post_init__(self):
|
||||
if self.start_date and self.start_time:
|
||||
self.start_ts = datetime.strptime(f'{self.start_date} {self.start_time}', '%Y.%m.%d %H:%M').timestamp()
|
||||
else:
|
||||
ts = parse_ts(f'{self.start_date} {self.start_time}', PO_DATETIME_FORMAT)
|
||||
if ts is None:
|
||||
LOG.error(f'wrong start_date or start_time in {self}')
|
||||
else:
|
||||
self.start_ts = ts
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
"""Identity of the outage: the same address and start time"""
|
||||
return self.start_ts, self.city, self.street, self.house
|
||||
|
||||
def set_sent(self):
|
||||
self.need_to_send = False
|
||||
@@ -45,17 +65,44 @@ class GPVData:
|
||||
hours_off: List
|
||||
hours_on: List
|
||||
outage_queue: str = field(repr=False)
|
||||
notified_at: datetime = field(repr=False, default=None)
|
||||
updated_at: datetime = field(repr=False, default=datetime.now())
|
||||
need_to_send: bool = field(repr=False, default=True)
|
||||
start_ts: List[float] = field(default_factory=list, init=False)
|
||||
notified_at: Optional[datetime] = field(repr=False, default=None, compare=False)
|
||||
updated_at: datetime = field(repr=False, default_factory=datetime.now, compare=False)
|
||||
need_to_send: bool = field(repr=False, default=True, compare=False)
|
||||
# timestamps of power-on moments (ends of outages)
|
||||
start_ts: List[float] = field(default_factory=list, init=False, compare=False)
|
||||
|
||||
def __post_init__(self):
|
||||
if self.event_date and self.hours_off and self.hours_on:
|
||||
for index, value in enumerate(self.hours_on):
|
||||
self.start_ts.append(datetime.strptime(f'{self.event_date} {self.hours_on[index]}', '%d.%m.%Y %H:%M').timestamp())
|
||||
else:
|
||||
LOG.error(f'wrong start_date or start_time in {self}')
|
||||
day_start = parse_ts(f'{self.event_date} 00:00', GPV_DATETIME_FORMAT)
|
||||
if day_start is None:
|
||||
LOG.error(f'wrong event_date in {self}')
|
||||
return
|
||||
for hour_off, hour_on in zip(self.hours_off, self.hours_on):
|
||||
ts_off = parse_ts(f'{self.event_date} {hour_off}', GPV_DATETIME_FORMAT)
|
||||
ts_on = parse_ts(f'{self.event_date} {hour_on}', GPV_DATETIME_FORMAT)
|
||||
if ts_on is None:
|
||||
LOG.error(f'wrong time {hour_on!r} in {self}')
|
||||
continue
|
||||
if ts_off is not None and ts_on <= ts_off:
|
||||
# e.g. '22:00-00:00': power is on at the next day
|
||||
ts_on = (datetime.fromtimestamp(ts_on) + timedelta(days=1)).timestamp()
|
||||
self.start_ts.append(ts_on)
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
return self.event_date, self.outage_queue
|
||||
|
||||
@property
|
||||
def actual_till_ts(self) -> Optional[float]:
|
||||
"""Moment after which the schedule is not actual: the last power-on or end of the event day"""
|
||||
if self.start_ts:
|
||||
return max(self.start_ts)
|
||||
day_start = parse_ts(f'{self.event_date} 00:00', GPV_DATETIME_FORMAT)
|
||||
if day_start is None:
|
||||
return None
|
||||
return (datetime.fromtimestamp(day_start) + timedelta(days=1)).timestamp()
|
||||
|
||||
def is_same_schedule(self, other: Self) -> bool:
|
||||
return self.hours_off == other.hours_off and self.hours_on == other.hours_on
|
||||
|
||||
def set_send(self):
|
||||
self.need_to_send = False
|
||||
@@ -66,7 +113,7 @@ class GPVData:
|
||||
class JobData:
|
||||
name: str
|
||||
trigger: str
|
||||
next: str
|
||||
next: Optional[datetime]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user