"""Agenda data.""" import asyncio import collections import os import typing from datetime import date, datetime, timedelta import dateutil.rrule import dateutil.tz import flask import holidays # type: ignore import isodate # type: ignore import lxml import pytz import yaml from . import ( accommodation, birthday, calendar, conference, domains, economist, fx, gwr, hn, meetup, stock_market, subscription, sun, thespacedevs, travel, uk_holiday, uk_midnight, waste_schedule, ) from .types import Event, Holiday here = dateutil.tz.tzlocal() # deadline to file tax return # credit card expiry dates # morzine ski lifts # chalet availablity calendar # starlink visible def timezone_transition( start: datetime, end: datetime, key: str, tz_name: str ) -> list[Event]: """Clocks changes.""" tz = pytz.timezone(tz_name) return [ Event(name=key, date=pytz.utc.localize(t).astimezone(tz)) for t in tz._utc_transition_times # type: ignore if start <= t <= end ] def us_holidays(start_date: date, end_date: date) -> list[Holiday]: """Get US holidays.""" found: list[Holiday] = [] for year in range(start_date.year, end_date.year + 1): hols = holidays.country_holidays("US", years=year, language="en") found += [ Holiday(date=hol_date, name=title.replace("'", "’"), country="us") for hol_date, title in hols.items() if start_date < hol_date < end_date ] extra = [] for h in found: if h.name != "Thanksgiving": continue extra += [ Holiday(date=h.date + timedelta(days=1), name="Black Friday", country="us"), Holiday(date=h.date + timedelta(days=4), name="Cyber Monday", country="us"), ] return found + extra def get_holidays(country: str, start_date: date, end_date: date) -> list[Holiday]: """Get holidays.""" found: list[Holiday] = [] for year in range(start_date.year, end_date.year + 1): hols = holidays.country_holidays(country.upper(), years=year, language="en_US") found += [ Holiday( date=hol_date, name=title, country=country.lower(), ) for hol_date, title in hols.items() if start_date < hol_date < end_date ] return found def dates_from_rrule( rrule: str, start: date, end: date ) -> typing.Sequence[datetime | date]: """Generate events from an RRULE between start_date and end_date.""" all_day = not any(param in rrule for param in ["BYHOUR", "BYMINUTE", "BYSECOND"]) return [ i.date() if all_day else i for i in dateutil.rrule.rrulestr(rrule, dtstart=uk_midnight(start)).between( uk_midnight(start), uk_midnight(end) ) ] async def waste_collection_events(data_dir: str) -> list[Event]: """Waste colllection events.""" postcode = "BS48 3HG" uprn = "24071046" html = await waste_schedule.get_html(data_dir, postcode, uprn) root = lxml.html.fromstring(html) events = waste_schedule.parse(root) return events async def bristol_waste_collection_events( data_dir: str, start_date: date ) -> list[Event]: """Waste colllection events.""" uprn = "358335" return await waste_schedule.get_bristol_gov_uk(start_date, data_dir, uprn) def combine_holidays(holidays: list[Holiday]) -> list[Event]: """Combine UK and US holidays with the same date and title.""" combined: collections.defaultdict[ tuple[date, str], set[str] ] = collections.defaultdict(set) for h in holidays: assert isinstance(h.name, str) and isinstance(h.date, date) event_key = (h.date, h.name) combined[event_key].add(h.country) events: list[Event] = [] for (d, name), countries in combined.items(): title = f'{name} ({", ".join(country.upper() for country in countries)})' e = Event(name="holiday", date=d, title=title) events.append(e) return events def get_yaml_event_date_field(item: dict[str, str]) -> str: """Event date field name.""" return ( "end_date" if item["name"] == "travel_insurance" else ("start_date" if "start_date" in item else "date") ) def get_yaml_event_end_date_field(item: dict[str, str]) -> str: """Event date field name.""" return ( "end_date" if item["name"] == "travel_insurance" else ("start_date" if "start_date" in item else "date") ) def read_events_yaml(data_dir: str, start: date, end: date) -> list[Event]: """Read eventes from YAML file.""" events: list[Event] = [] for item in yaml.safe_load(open(os.path.join(data_dir, "events.yaml"))): duration = ( isodate.parse_duration(item["duration"]) if "duration" in item else None ) dates = ( dates_from_rrule(item["rrule"], start, end) if "rrule" in item else [item[get_yaml_event_date_field(item)]] ) for dt in dates: e = Event( name=item["name"], date=dt, end_date=( dt + duration if duration else ( item.get("end_date") if item["name"] != "travel_insurance" else None ) ), title=item.get("title"), url=item.get("url"), ) events.append(e) return events async def get_data( now: datetime, config: flask.config.Config ) -> typing.Mapping[str, str | object]: """Get data to display on agenda dashboard.""" data_dir = config["DATA_DIR"] rocket_dir = os.path.join(data_dir, "thespacedevs") today = now.date() two_weeks_ago = today - timedelta(weeks=2) last_week = today - timedelta(weeks=1) last_year = today - timedelta(days=365) next_year = today + timedelta(days=365) minus_365 = now - timedelta(days=365) plus_365 = now + timedelta(days=365) ( gbpusd, gwr_advance_tickets, bank_holiday, rockets, backwell_bins, bristol_bins, ) = await asyncio.gather( fx.get_gbpusd(config), gwr.advance_ticket_date(data_dir), uk_holiday.bank_holiday_list(last_year, next_year, data_dir), thespacedevs.get_launches(rocket_dir, limit=40), waste_collection_events(data_dir), bristol_waste_collection_events(data_dir, today), ) reply = { "now": now, "gbpusd": gbpusd, "stock_markets": stock_market.open_and_close(), "rockets": rockets, "gwr_advance_tickets": gwr_advance_tickets, } my_data = config["PERSONAL_DATA"] events = ( [ Event(name="mothers_day", date=uk_holiday.get_mothers_day(today)), ] + timezone_transition(minus_365, plus_365, "uk_clock_change", "Europe/London") + timezone_transition( minus_365, plus_365, "us_clock_change", "America/New_York" ) ) if gwr_advance_tickets: events.append(Event(name="gwr_advance_tickets", date=gwr_advance_tickets)) holidays: list[Holiday] = bank_holiday + us_holidays(last_year, next_year) for country in ( "at", "be", "ch", "cz", "de", "dk", "ee", "es", "fr", "gr", "it", "ke", "nl", "pl", ): holidays += get_holidays(country, last_year, next_year) events += combine_holidays(holidays) events += birthday.get_birthdays(last_year, os.path.join(my_data, "entities.yaml")) events += accommodation.get_events(os.path.join(my_data, "accommodation.yaml")) events += travel.all_events(my_data) events += conference.get_list(os.path.join(my_data, "conferences.yaml")) events += backwell_bins + bristol_bins events += read_events_yaml(my_data, last_year, next_year) events += subscription.get_events(os.path.join(my_data, "subscriptions.yaml")) events += economist.publication_dates(last_week, next_year) events += meetup.get_events(my_data) events += hn.whoishiring(last_year, next_year) events += domains.renewal_dates(my_data) for launch in rockets: dt = None if launch["net_precision"] == "Day": dt = datetime.strptime(launch["net"], "%Y-%m-%dT00:00:00Z").date() elif launch["t0_time"]: dt = pytz.utc.localize( datetime.strptime(launch["net"], "%Y-%m-%dT%H:%M:%SZ") ) if not dt: continue rocket_name = f'🚀{launch["rocket"]}: {launch["mission_name"] or "[no mission]"}' e = Event(name="rocket", date=dt, title=rocket_name) events.append(e) events += [Event(name="today", date=today)] events.sort(key=lambda e: (e.as_datetime, e.name != "today")) observer = sun.bristol() reply["sunrise"] = sun.sunrise(observer) reply["sunset"] = sun.sunset(observer) reply["events"] = events reply["last_week"] = last_week reply["two_weeks_ago"] = two_weeks_ago reply["fullcalendar_events"] = calendar.build_events(events) return reply