parent
							
								
									ae8ed755c4
								
							
						
					
					
						commit
						4ee81de1fd
					
				| 
						 | 
					@ -594,20 +594,28 @@ def waste_collection_events() -> list[Event]:
 | 
				
			||||||
    return events
 | 
					    return events
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def bristol():
 | 
					def bristol_waste_collection_events(start_date: date) -> list[Event]:
 | 
				
			||||||
 | 
					    """Waste colllection events."""
 | 
				
			||||||
 | 
					    uprn = "358335"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return waste_schedule.get_bristol_gov_uk(start_date, data_dir, uprn)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def bristol() -> ephem.Observer:
 | 
				
			||||||
 | 
					    """Location of Bristol."""
 | 
				
			||||||
    observer = ephem.Observer()
 | 
					    observer = ephem.Observer()
 | 
				
			||||||
    observer.lat, observer.lon = "51.4545", "-2.5879"
 | 
					    observer.lat, observer.lon = "51.4545", "-2.5879"
 | 
				
			||||||
    return observer
 | 
					    return observer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def sunrise(observer):
 | 
					def sunrise(observer: ephem.Observer) -> datetime:
 | 
				
			||||||
    """Sunrise."""
 | 
					    """Sunrise."""
 | 
				
			||||||
    return observer.next_rising(ephem.Sun(observer)).datetime()
 | 
					    return typing.cast(datetime, observer.next_rising(ephem.Sun(observer)).datetime())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def sunset(observer):
 | 
					def sunset(observer: ephem.Observer) -> datetime:
 | 
				
			||||||
    """Sunrise."""
 | 
					    """Sunrise."""
 | 
				
			||||||
    return observer.next_setting(ephem.Sun(observer)).datetime()
 | 
					    return typing.cast(datetime, observer.next_setting(ephem.Sun(observer)).datetime())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_data(now: datetime) -> typing.Mapping[str, str | object]:
 | 
					def get_data(now: datetime) -> typing.Mapping[str, str | object]:
 | 
				
			||||||
| 
						 | 
					@ -663,7 +671,7 @@ def get_data(now: datetime) -> typing.Mapping[str, str | object]:
 | 
				
			||||||
    events += get_accommodation(today, os.path.join(my_data, "accommodation.yaml"))
 | 
					    events += get_accommodation(today, os.path.join(my_data, "accommodation.yaml"))
 | 
				
			||||||
    events += get_all_travel_events(today)
 | 
					    events += get_all_travel_events(today)
 | 
				
			||||||
    events += get_conferences(today, os.path.join(my_data, "conferences.yaml"))
 | 
					    events += get_conferences(today, os.path.join(my_data, "conferences.yaml"))
 | 
				
			||||||
    events += waste_collection_events()
 | 
					    events += waste_collection_events() + bristol_waste_collection_events(today)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    next_up_series = Event(
 | 
					    next_up_series = Event(
 | 
				
			||||||
        date=date(2026, 6, 1),
 | 
					        date=date(2026, 6, 1),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
#!/usr/bin/python3
 | 
					#!/usr/bin/python3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					import typing
 | 
				
			||||||
from collections import defaultdict
 | 
					from collections import defaultdict
 | 
				
			||||||
from datetime import date, datetime, timedelta
 | 
					from datetime import date, datetime, timedelta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,13 +11,23 @@ import requests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .types import Event
 | 
					from .types import Event
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ttl_hours = 6
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def make_waste_dir(data_dir: str) -> None:
 | 
				
			||||||
 | 
					    """Make waste dir if missing."""
 | 
				
			||||||
 | 
					    waste_dir = os.path.join(data_dir, "waste")
 | 
				
			||||||
 | 
					    if not os.path.exists(waste_dir):
 | 
				
			||||||
 | 
					        os.mkdir(waste_dir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_html(data_dir: str, postcode: str, uprn: str) -> str:
 | 
					def get_html(data_dir: str, postcode: str, uprn: str) -> str:
 | 
				
			||||||
    """Get waste schedule."""
 | 
					    """Get waste schedule."""
 | 
				
			||||||
    now = datetime.now()
 | 
					    now = datetime.now()
 | 
				
			||||||
    waste_dir = os.path.join(data_dir, "waste")
 | 
					    waste_dir = os.path.join(data_dir, "waste")
 | 
				
			||||||
    if not os.path.exists(waste_dir):
 | 
					
 | 
				
			||||||
        os.mkdir(waste_dir)
 | 
					    make_waste_dir(data_dir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    existing_data = os.listdir(waste_dir)
 | 
					    existing_data = os.listdir(waste_dir)
 | 
				
			||||||
    existing = [f for f in existing_data if f.endswith(".html")]
 | 
					    existing = [f for f in existing_data if f.endswith(".html")]
 | 
				
			||||||
    if existing:
 | 
					    if existing:
 | 
				
			||||||
| 
						 | 
					@ -23,7 +35,7 @@ def get_html(data_dir: str, postcode: str, uprn: str) -> str:
 | 
				
			||||||
        recent = datetime.strptime(recent_filename, "%Y-%m-%d_%H:%M.html")
 | 
					        recent = datetime.strptime(recent_filename, "%Y-%m-%d_%H:%M.html")
 | 
				
			||||||
        delta = now - recent
 | 
					        delta = now - recent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if existing and delta < timedelta(hours=6):
 | 
					    if existing and delta < timedelta(hours=ttl_hours):
 | 
				
			||||||
        return open(os.path.join(waste_dir, recent_filename)).read()
 | 
					        return open(os.path.join(waste_dir, recent_filename)).read()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    now_str = now.strftime("%Y-%m-%d_%H:%M")
 | 
					    now_str = now.strftime("%Y-%m-%d_%H:%M")
 | 
				
			||||||
| 
						 | 
					@ -69,6 +81,110 @@ def parse(root: lxml.html.HtmlElement) -> list[Event]:
 | 
				
			||||||
        by_date[following_date].append(service)
 | 
					        by_date[following_date].append(service)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return [
 | 
					    return [
 | 
				
			||||||
        Event(name="waste_schedule", date=d, title=", ".join(services))
 | 
					        Event(name="waste_schedule", date=d, title="Backwell: " + ", ".join(services))
 | 
				
			||||||
 | 
					        for d, services in by_date.items()
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					BristolSchedule = list[dict[str, typing.Any]]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_bristol_data(data_dir: str, uprn: str) -> 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if existing and delta < timedelta(hours=ttl_hours):
 | 
				
			||||||
 | 
					        json_data = json.load(open(os.path.join(waste_dir, recent_filename)))
 | 
				
			||||||
 | 
					        return typing.cast(BristolSchedule, json_data["data"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    now_str = now.strftime("%Y-%m-%d_%H:%M")
 | 
				
			||||||
 | 
					    filename = f"{waste_dir}/{now_str}_{uprn}.json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    r = get_bristol_gov_uk_data(uprn)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with open(filename, "wb") as out:
 | 
				
			||||||
 | 
					        out.write(r.content)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return typing.cast(BristolSchedule, r.json()["data"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_bristol_gov_uk_data(uprn: str) -> requests.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)
 | 
				
			||||||
 | 
					    s = requests.Session()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Initialise form
 | 
				
			||||||
 | 
					    payload = {"servicetypeid": "7dce896c-b3ba-ea11-a812-000d3a7f1cdc"}
 | 
				
			||||||
 | 
					    response = s.get(
 | 
				
			||||||
 | 
					        "https://bristolcouncil.powerappsportals.com/completedynamicformunauth/",
 | 
				
			||||||
 | 
					        headers=HEADERS,
 | 
				
			||||||
 | 
					        params=payload,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    host = "bcprdapidyna002.azure-api.net"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Set the search criteria
 | 
				
			||||||
 | 
					    payload = {"Uprn": "UPRN" + _uprn}
 | 
				
			||||||
 | 
					    response = s.post(
 | 
				
			||||||
 | 
					        f"https://{host}/bcprdfundyna001-llpg/DetailedLLPG",
 | 
				
			||||||
 | 
					        headers=HEADERS,
 | 
				
			||||||
 | 
					        json=payload,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Retrieve the schedule
 | 
				
			||||||
 | 
					    payload = {"uprn": _uprn}
 | 
				
			||||||
 | 
					    response = s.post(
 | 
				
			||||||
 | 
					        f"https://{host}/bcprdfundyna001-alloy/NextCollectionDates",
 | 
				
			||||||
 | 
					        headers=HEADERS,
 | 
				
			||||||
 | 
					        json=payload,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_bristol_gov_uk(start_date: date, data_dir: str, uprn: str) -> list[Event]:
 | 
				
			||||||
 | 
					    """Get waste collection schedule from Bristol City Council."""
 | 
				
			||||||
 | 
					    data = get_bristol_data(data_dir, uprn)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    by_date: defaultdict[date, list[str]] = defaultdict(list)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for item in data:
 | 
				
			||||||
 | 
					        service = item["containerName"]
 | 
				
			||||||
 | 
					        service = "Recycling" if "Recycling" in service else service.partition(" ")[2]
 | 
				
			||||||
 | 
					        for collection in item["collection"]:
 | 
				
			||||||
 | 
					            for collection_date_key in ["nextCollectionDate", "lastCollectionDate"]:
 | 
				
			||||||
 | 
					                d = date.fromisoformat(collection[collection_date_key][:10])
 | 
				
			||||||
 | 
					                if d < start_date:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					                if 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()
 | 
					        for d, services in by_date.items()
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue