agenda/agenda/types.py

291 lines
8.2 KiB
Python

"""Types."""
import datetime
import typing
from dataclasses import dataclass, field
from pycountry.db import Country
import agenda
from agenda import format_list_with_ampersand
StrDict = dict[str, typing.Any]
DateOrDateTime = datetime.datetime | datetime.date
def as_date(d: DateOrDateTime) -> datetime.date:
"""Convert datetime to date."""
if isinstance(d, datetime.datetime):
return d.date()
assert isinstance(d, datetime.date)
return d
@dataclass
class Trip:
"""Trip."""
start: datetime.date
travel: list[StrDict] = field(default_factory=list)
accommodation: list[StrDict] = field(default_factory=list)
conferences: list[StrDict] = field(default_factory=list)
events: list[StrDict] = field(default_factory=list)
name: str | None = None
private: bool = False
@property
def title(self) -> str:
"""Trip title."""
if self.name:
return self.name
titles: list[str] = [conf["name"] for conf in self.conferences] + [
event["title"] for event in self.events
]
if not titles:
for travel in self.travel:
if travel["depart"] and travel["depart"].date() != self.start:
place = travel["from"]
if place not in titles:
titles.append(place)
if travel["depart"] and travel["depart"].date() != self.end:
place = travel["to"]
if place not in titles:
titles.append(place)
return format_list_with_ampersand(titles) or "[unnamed trip]"
@property
def end(self) -> datetime.date | None:
"""End date for trip."""
max_conference_end = (
max(as_date(item["end"]) for item in self.conferences)
if self.conferences
else datetime.date.min
)
assert isinstance(max_conference_end, datetime.date)
arrive = [as_date(item["arrive"]) for item in self.travel if "arrive" in item]
travel_end = max(arrive) if arrive else datetime.date.min
assert isinstance(travel_end, datetime.date)
accommodation_end = (
max(as_date(item["to"]) for item in self.accommodation)
if self.accommodation
else datetime.date.min
)
assert isinstance(accommodation_end, datetime.date)
max_date = max(max_conference_end, travel_end, accommodation_end)
return max_date if max_date != datetime.date.min else None
def locations(self) -> list[tuple[str, Country]]:
"""Locations for trip."""
seen: set[tuple[str, str]] = set()
items = []
for item in self.conferences + self.accommodation + self.events:
if "country" not in item or "location" not in item:
continue
key = (item["location"], item["country"])
if key in seen:
continue
seen.add(key)
country = agenda.get_country(item["country"])
assert country
items.append((item["location"], country))
return items
@property
def countries(self) -> list[Country]:
"""Countries visited as part of trip, in order."""
seen: set[str] = set()
items: list[Country] = []
for item in self.conferences + self.accommodation + self.events:
if "country" not in item:
continue
if item["country"] in seen:
continue
seen.add(item["country"])
country = agenda.get_country(item["country"])
assert country
items.append(country)
return items
@property
def countries_str(self) -> str:
"""List of countries visited on this trip."""
return format_list_with_ampersand(
[f"{c.name} {c.flag}" for c in self.countries]
)
@property
def locations_str(self) -> str:
"""List of countries visited on this trip."""
return format_list_with_ampersand(
[f"{location} ({c.name}) {c.flag}" for location, c in self.locations()]
)
@property
def country_flags(self) -> str:
"""Countries flags for trip."""
return "".join(c.flag for c in self.countries)
def total_distance(self) -> float | None:
"""Total distance for trip."""
return (
sum(t["distance"] for t in self.travel)
if all(t.get("distance") for t in self.travel)
else None
)
@dataclass
class Holiday:
"""Holiay."""
name: str
country: str
date: datetime.date
local_name: str | None = None
@property
def display_name(self) -> str:
"""Format name for display."""
return (
f"{self.name} ({self.local_name})"
if self.local_name and self.local_name != self.name
else self.name
)
emojis = {
"market": "🧺",
"us_presidential_election": "🗳️🇺🇸",
"bus_route_closure": "🚌❌",
"meetup": "👥",
"dinner": "🍷",
"party": "🍷",
"ba_voucher": "✈️",
"accommodation": "🏨", # alternative: 🧳
"flight": "✈️",
"conference": "🎤",
"rocket": "🚀",
"birthday": "🎈",
"waste_schedule": "🗑️",
"economist": "📰",
"running": "🏃",
"critical_mass": "🚴",
"trip": "🧳",
"hackathon": "💻",
}
@dataclass
class Event:
"""Event."""
name: str
date: DateOrDateTime
end_date: DateOrDateTime | None = None
title: str | None = None
url: str | None = None
going: bool | None = None
@property
def as_datetime(self) -> datetime.datetime:
"""Date/time of event."""
d = self.date
t0 = datetime.datetime.min.time()
return (
d
if isinstance(d, datetime.datetime)
else datetime.datetime.combine(d, t0).replace(tzinfo=datetime.timezone.utc)
)
@property
def has_time(self) -> bool:
"""Event has a time associated with it."""
return isinstance(self.date, datetime.datetime)
@property
def as_date(self) -> datetime.date:
"""Date of event."""
return (
self.date.date() if isinstance(self.date, datetime.datetime) else self.date
)
@property
def end_as_date(self) -> datetime.date:
"""Date of event."""
return (
(
self.end_date.date()
if isinstance(self.end_date, datetime.datetime)
else self.end_date
)
if self.end_date
else self.as_date
)
@property
def display_time(self) -> str | None:
"""Time for display on web page."""
return (
self.date.strftime("%H:%M")
if isinstance(self.date, datetime.datetime)
else None
)
@property
def display_timezone(self) -> str | None:
"""Timezone for display on web page."""
return (
self.date.strftime("%z")
if isinstance(self.date, datetime.datetime)
else None
)
def delta_days(self, today: datetime.date) -> str:
"""Return number of days from today as a string."""
delta = (self.as_date - today).days
match delta:
case 0:
return "today"
case 1:
return "1 day"
case _:
return f"{delta:,d} days"
@property
def display_date(self) -> str:
"""Date for display on web page."""
if isinstance(self.date, datetime.datetime):
return self.date.strftime("%a, %d, %b %Y %H:%M %z")
else:
return self.date.strftime("%a, %d, %b %Y")
@property
def display_title(self) -> str:
"""Name for display."""
return self.title or self.name
@property
def emoji(self) -> str | None:
"""Emoji."""
if self.title == "LHG Run Club":
return "🏃🍻"
return emojis.get(self.name)
@property
def title_with_emoji(self) -> str | None:
"""Title with optional emoji at the start."""
title = self.title or self.name
if title is None:
return None
emoji = self.emoji
return f"{emoji} {title}" if emoji else title