lazyflat: combined alert + apply behind authenticated web UI
Three isolated services (alert scraper, apply HTTP worker, web UI+DB) with argon2 auth, signed cookies, CSRF, rate-limited login, kill switch, apply circuit breaker, audit log, and strict CSP. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
69f2f1f635
46 changed files with 4183 additions and 0 deletions
100
alert/main.py
Normal file
100
alert/main.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import logging
|
||||
import time
|
||||
from rich.console import Console
|
||||
from rich.logging import RichHandler
|
||||
|
||||
from flat import Flat
|
||||
from scraper import Scraper
|
||||
from settings import TIME_INTERVALL
|
||||
from utils import hash_any_object
|
||||
from web_client import WebClient
|
||||
|
||||
|
||||
def setup_logging():
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(message)s",
|
||||
datefmt="[%X]",
|
||||
handlers=[RichHandler(markup=True, console=Console(width=110))],
|
||||
)
|
||||
logging.getLogger("googlemaps").setLevel(logging.WARNING)
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
logger = logging.getLogger("alert")
|
||||
setup_logging()
|
||||
|
||||
|
||||
class FlatAlerter:
|
||||
def __init__(self):
|
||||
self.web = WebClient()
|
||||
self.last_response_hash = ""
|
||||
|
||||
def _flat_payload(self, flat: Flat) -> dict:
|
||||
c = flat.connectivity
|
||||
return {
|
||||
"id": flat.id,
|
||||
"link": flat.link,
|
||||
"address": flat.address,
|
||||
"rooms": flat.rooms,
|
||||
"size": flat.size,
|
||||
"cold_rent": flat.cold_rent,
|
||||
"utilities": flat.utilities,
|
||||
"total_rent": flat.total_rent,
|
||||
"sqm_price": flat.sqm_price,
|
||||
"available_from": flat.available_from,
|
||||
"published_on": flat.published_on,
|
||||
"wbs": flat.wbs,
|
||||
"floor": flat.floor,
|
||||
"bathrooms": flat.bathrooms,
|
||||
"year_built": flat.year_built,
|
||||
"heating": flat.heating,
|
||||
"energy_carrier": flat.energy_carrier,
|
||||
"energy_value": flat.energy_value,
|
||||
"energy_certificate": flat.energy_certificate,
|
||||
"address_link_gmaps": flat.address_link_gmaps,
|
||||
"connectivity": {
|
||||
"morning_time": c.get("morning_time", 0),
|
||||
"morning_transfers": c.get("morning_transfers", 0),
|
||||
"night_time": c.get("night_time", 0),
|
||||
"night_transfers": c.get("night_transfers", 0),
|
||||
},
|
||||
"raw_data": flat.raw_data,
|
||||
}
|
||||
|
||||
def scan(self):
|
||||
logger.info("starting scan")
|
||||
scraper = Scraper()
|
||||
if not scraper.login():
|
||||
return
|
||||
flats_data = scraper.get_flats()
|
||||
|
||||
response_hashed = hash_any_object(flats_data)
|
||||
if response_hashed == self.last_response_hash:
|
||||
logger.info("no change since last scan")
|
||||
return
|
||||
self.last_response_hash = response_hashed
|
||||
|
||||
for number, data in enumerate(flats_data, 1):
|
||||
flat = Flat(data)
|
||||
logger.info(f"{str(number).rjust(2)}: submitting {flat}")
|
||||
payload = self._flat_payload(flat)
|
||||
if not self.web.submit_flat(payload):
|
||||
logger.warning(f"\tcould not submit {flat.id} to web, will retry next loop")
|
||||
logger.info("scan finished")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("starting lazyflat alert service")
|
||||
alerter = FlatAlerter()
|
||||
while True:
|
||||
try:
|
||||
alerter.scan()
|
||||
alerter.web.heartbeat()
|
||||
logger.info(f"sleeping for {TIME_INTERVALL} seconds")
|
||||
time.sleep(TIME_INTERVALL)
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
except Exception:
|
||||
logger.exception("unexpected error")
|
||||
time.sleep(60)
|
||||
Loading…
Add table
Add a link
Reference in a new issue