agenda/agenda/bristol_waste.py

130 lines
4.1 KiB
Python

"""Waste collection schedules."""
import json
import os
import typing
from collections import defaultdict
from datetime import date, datetime, timedelta
import httpx
from .event import Event
from .utils import make_waste_dir
ttl_hours = 12
BristolSchedule = list[dict[str, typing.Any]]
async def get(
start_date: date, data_dir: str, uprn: str, refresh: bool = False
) -> list[Event]:
"""Get waste collection schedule from Bristol City Council."""
by_date: defaultdict[date, list[str]] = defaultdict(list)
for item in await get_data(data_dir, uprn, refresh):
service = get_service(item)
for d in collections(item):
if d < start_date and service not in by_date[d]:
by_date[d].append(service)
return [
Event(name="waste_schedule", date=d, title="Bristol: " + ", ".join(services))
for d, services in by_date.items()
]
async def get_data(data_dir: str, uprn: str, refresh: bool = False) -> BristolSchedule:
"""Get Bristol Waste schedule, with cache."""
now = datetime.now()
waste_dir = os.path.join(data_dir, "waste")
make_waste_dir(data_dir)
existing_data = os.listdir(waste_dir)
existing = [f for f in existing_data if f.endswith(f"_{uprn}.json")]
if existing:
recent_filename = max(existing)
recent = datetime.strptime(recent_filename, f"%Y-%m-%d_%H:%M_{uprn}.json")
delta = now - recent
def get_from_recent() -> BristolSchedule:
json_data = json.load(open(os.path.join(waste_dir, recent_filename)))
return typing.cast(BristolSchedule, json_data["data"])
if not refresh and existing and delta < timedelta(hours=ttl_hours):
return get_from_recent()
try:
r = await get_web_data(uprn)
except httpx.ReadTimeout:
return get_from_recent()
with open(f'{waste_dir}/{now.strftime("%Y-%m-%d_%H:%M")}_{uprn}.json', "wb") as out:
out.write(r.content)
return typing.cast(BristolSchedule, r.json()["data"])
async def get_web_data(uprn: str) -> httpx.Response:
"""Get JSON from Bristol City Council."""
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
HEADERS = {
"Accept": "*/*",
"Accept-Language": "en-GB,en;q=0.9",
"Connection": "keep-alive",
"Ocp-Apim-Subscription-Key": "47ffd667d69c4a858f92fc38dc24b150",
"Ocp-Apim-Trace": "true",
"Origin": "https://bristolcouncil.powerappsportals.com",
"Referer": "https://bristolcouncil.powerappsportals.com/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site",
"Sec-GPC": "1",
"User-Agent": UA,
}
_uprn = str(uprn).zfill(12)
async with httpx.AsyncClient(timeout=20) as client:
# Initialise form
payload = {"servicetypeid": "7dce896c-b3ba-ea11-a812-000d3a7f1cdc"}
response = await client.get(
"https://bristolcouncil.powerappsportals.com/completedynamicformunauth/",
headers=HEADERS,
params=payload,
)
host = "bcprdapidyna002.azure-api.net"
# Set the search criteria
payload = {"Uprn": "UPRN" + _uprn}
response = await client.post(
f"https://{host}/bcprdfundyna001-llpg/DetailedLLPG",
headers=HEADERS,
json=payload,
)
# Retrieve the schedule
payload = {"uprn": _uprn}
response = await client.post(
f"https://{host}/bcprdfundyna001-alloy/NextCollectionDates",
headers=HEADERS,
json=payload,
)
return response
def get_service(item: dict[str, typing.Any]) -> str:
"""Bristol waste service name."""
service: str = item["containerName"]
return "Recycling" if "Recycling" in service else service.partition(" ")[2]
def collections(item: dict[str, typing.Any]) -> typing.Iterable[date]:
"""Bristol dates from collections."""
for collection in item["collection"]:
for collection_date_key in ["nextCollectionDate", "lastCollectionDate"]:
yield date.fromisoformat(collection[collection_date_key][:10])