import configparser import json import os import typing import warnings from datetime import date, datetime, timedelta, timezone from decimal import Decimal from time import time as unixtime import dateutil import dateutil.parser import exchange_calendars import holidays import pandas import pytz import requests # from agenda import spacexdata warnings.simplefilter(action="ignore", category=FutureWarning) # deadline to file tax return # credit card expiry dates # morzine ski lifts # chalet availablity calendar # sunrise and sunset # starlink visible here = dateutil.tz.tzlocal() now = datetime.now() today = now.date() now_str = now.strftime("%Y-%m-%d_%H:%M") now_utc = datetime.now(timezone.utc) next_us_presidential_election = date(2024, 11, 5) next_uk_general_election = date(2024, 5, 2) config_filename = os.path.join(os.path.dirname(__file__), "..", "config") assert os.path.exists(config_filename) config = configparser.ConfigParser() config.read(config_filename) access_key = config.get("exchangerate", "access_key") data_dir = config.get("data", "dir") def get_next_timezone_transition(tz_name: str) -> datetime: """Datetime of the next time the clocks change.""" tz = pytz.timezone(tz_name) dt = next(t for t in tz._utc_transition_times if t > now) return typing.cast(datetime, dt) def get_next_bank_holiday() -> dict[str, date | str]: """Date and name of the next UK bank holiday.""" url = "https://www.gov.uk/bank-holidays.json" filename = os.path.join(data_dir, "bank-holidays.json") mtime = os.path.getmtime(filename) if (unixtime() - mtime) > 3600: # one hour r = requests.get(url) open(filename, "w").write(r.text) events = json.load(open(filename))["england-and-wales"]["events"] next_holiday = None for event in events: event_date = datetime.strptime(event["date"], "%Y-%m-%d").date() if event_date < today: continue next_holiday = {"date": event_date, "title": event["title"]} break assert next_holiday return next_holiday def get_gbpusd() -> Decimal: """Get the current value for GBPUSD, with caching.""" fx_dir = os.path.join(data_dir, "fx") existing_data = os.listdir(fx_dir) existing = [f for f in existing_data if f.endswith("_GBPUSD.json")] if existing: recent_filename = max(existing) recent = datetime.strptime(recent_filename, "%Y-%m-%d_%H:%M_GBPUSD.json") delta = now - recent if existing and delta < timedelta(hours=6): full = os.path.join(fx_dir, recent_filename) data = json.load(open(full), parse_float=Decimal) if "quotes" in data and "USDGBP" in data["quotes"]: return typing.cast(Decimal, 1 / data["quotes"]["USDGBP"]) url = "http://api.exchangerate.host/live" params = {"currencies": "GBP,USD", "access_key": access_key} filename = f"{fx_dir}/{now_str}_GBPUSD.json" r = requests.get(url, params=params) open(filename, "w").write(r.text) data = json.loads(r.text, parse_float=Decimal) return typing.cast(Decimal, 1 / data["quotes"]["USDGBP"]) def next_economist() -> date: """Next date that the Economist is published.""" # TODO: handle the Christmas double issue correctly return today + timedelta((3 - today.weekday()) % 7) def timedelta_display(delta: timedelta) -> str: """Format timedelta as a human readable string.""" total_seconds = int(delta.total_seconds()) days, remainder = divmod(total_seconds, 24 * 60 * 60) hours, remainder = divmod(remainder, 60 * 60) mins, secs = divmod(remainder, 60) return " ".join( f"{v:>3} {label}" for v, label in ((days, "days"), (hours, "hrs"), (mins, "mins")) if v ) def stock_markets() -> list[str]: """Stock markets open and close times.""" # The trading calendars code is slow, maybe there is a faster way to do this # Or we could cache the result now = pandas.Timestamp.now(timezone.utc) now_local = pandas.Timestamp.now(here) markets = [ ("XLON", "London"), ("XNYS", "US"), ] reply = [] for code, label in markets: cal = exchange_calendars.get_calendar(code) if cal.is_open_on_minute(now_local): next_close = cal.next_close(now).tz_convert(here) next_close = next_close.replace(minute=round(next_close.minute, -1)) delta_close = timedelta_display(next_close - now_local) prev_open = cal.previous_open(now).tz_convert(here) prev_open = prev_open.replace(minute=round(prev_open.minute, -1)) delta_open = timedelta_display(now_local - prev_open) msg = ( f"{label:>6} market opened {delta_open} ago, " + f"closes in {delta_close} ({next_close:%H:%M})" ) else: ts = cal.next_open(now) ts = ts.replace(minute=round(ts.minute, -1)) ts = ts.tz_convert(here) delta = timedelta_display(ts - now_local) msg = f"{label:>6} market opens in {delta}" + ( f" ({ts:%H:%M})" if (ts - now_local) < timedelta(days=1) else "" ) reply.append(msg) return reply def get_us_holiday() -> dict[str, date | str]: """Date and name of next US holiday.""" hols = holidays.UnitedStates(years=[today.year, today.year + 1]) next_hol = next(x for x in sorted(hols.items()) if x[0] >= today) return {"date": next_hol[0], "title": next_hol[1]} def get_data() -> dict[str, str | object]: """Get data to display on agenda dashboard.""" reply = { "now": now, "gbpusd": get_gbpusd(), "next_economist": next_economist(), "bank_holiday": get_next_bank_holiday(), "us_holiday": get_us_holiday(), "next_uk_general_election": next_uk_general_election, "next_us_presidential_election": next_us_presidential_election, # "spacex": spacexdata.get_next_spacex_launch(limit=20), "stock_markets": stock_markets(), "uk_clock_change": get_next_timezone_transition("Europe/London"), "us_clock_change": get_next_timezone_transition("America/New_York"), } return reply