File size: 6,072 Bytes
102dd4f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | from __future__ import annotations
import re
from dataclasses import dataclass
from datetime import date, datetime, time
from typing import Any, Literal
from zoneinfo import ZoneInfo
APP_TIMEZONE = "Asia/Aden"
DEPARTURE_BUCKETS = ("morning", "noon", "night")
DepartureBucket = Literal["morning", "noon", "night"]
@dataclass(frozen=True)
class DepartureRequest:
departure_date: date | None = None
departure_time: DepartureBucket | None = None
def now_in_aden() -> datetime:
return datetime.now(tz=ZoneInfo(APP_TIMEZONE))
def normalize_departure_bucket(value: Any) -> DepartureBucket | None:
if value is None:
return None
text = str(value).strip().lower()
if text in DEPARTURE_BUCKETS:
return text # type: ignore[return-value]
parsed = _parse_datetime(text)
if parsed:
return bucket_for_time(parsed.astimezone(ZoneInfo(APP_TIMEZONE)).time())
parsed_time = _parse_time(text)
if parsed_time:
return bucket_for_time(parsed_time)
if "morning" in text or "صباح" in text:
return "morning"
if "noon" in text or "afternoon" in text or "ظهر" in text:
return "noon"
if "night" in text or "evening" in text or "ليل" in text or "مساء" in text:
return "night"
return None
def bucket_for_time(value: time) -> DepartureBucket:
if value.hour < 12:
return "morning"
if value.hour < 18:
return "noon"
return "night"
def parse_departure_request(
*,
travel_date: Any = None,
travel_time: Any = None,
travel_datetime: Any = None,
) -> DepartureRequest:
requested_date = _parse_date_value(travel_date)
requested_time = normalize_departure_bucket(travel_time)
if travel_datetime:
parsed_datetime = _parse_datetime(str(travel_datetime).strip())
if parsed_datetime:
aden_datetime = parsed_datetime.astimezone(ZoneInfo(APP_TIMEZONE))
requested_date = requested_date or aden_datetime.date()
requested_time = requested_time or bucket_for_time(aden_datetime.time())
else:
requested_date = requested_date or _parse_date_value(travel_datetime)
requested_time = requested_time or normalize_departure_bucket(travel_datetime)
return DepartureRequest(departure_date=requested_date, departure_time=requested_time)
def parse_requested_clock_time(
*,
travel_time: Any = None,
travel_datetime: Any = None,
exact_time: Any = None,
) -> time | None:
for value in (exact_time, travel_datetime, travel_time):
if not value:
continue
text = str(value).strip()
parsed_datetime = _parse_datetime(text)
if parsed_datetime:
return parsed_datetime.astimezone(ZoneInfo(APP_TIMEZONE)).time().replace(
second=0,
microsecond=0,
)
parsed_time = _parse_time(text)
if parsed_time:
return parsed_time
return None
def not_departed_bucket_filter(now: datetime | None = None) -> tuple[date, tuple[str, ...]]:
aden_now = (now or now_in_aden()).astimezone(ZoneInfo(APP_TIMEZONE))
if aden_now.time() < time(12, 0):
return aden_now.date(), DEPARTURE_BUCKETS
if aden_now.time() < time(18, 0):
return aden_now.date(), ("noon", "night")
return aden_now.date(), ("night",)
def trip_departure_date(trip: dict[str, Any]) -> date | None:
parsed = _parse_date_value(trip.get("departure_date"))
if parsed:
return parsed
parsed_datetime = _parse_datetime(str(trip.get("departure_time") or "").strip())
if parsed_datetime:
return parsed_datetime.astimezone(ZoneInfo(APP_TIMEZONE)).date()
return None
def trip_departure_bucket(trip: dict[str, Any]) -> DepartureBucket | None:
return normalize_departure_bucket(trip.get("departure_time"))
def trip_satisfies_departure_request(
trip: dict[str, Any],
request: DepartureRequest,
*,
now: datetime | None = None,
) -> bool:
trip_date = trip_departure_date(trip)
trip_bucket = trip_departure_bucket(trip)
if trip_date is None or trip_bucket is None:
return False
today, remaining_buckets = not_departed_bucket_filter(now)
if request.departure_date:
if trip_date != request.departure_date:
return False
if request.departure_time:
return trip_bucket == request.departure_time
if trip_date == today:
return trip_bucket in remaining_buckets
return trip_date > today
if request.departure_time and trip_bucket != request.departure_time:
return False
return trip_date > today or (trip_date == today and trip_bucket in remaining_buckets)
def _parse_date_value(value: Any) -> date | None:
if value is None:
return None
if isinstance(value, date) and not isinstance(value, datetime):
return value
if isinstance(value, datetime):
return value.astimezone(ZoneInfo(APP_TIMEZONE)).date()
text = str(value).strip()
if not text:
return None
match = re.search(r"\b(\d{4}-\d{2}-\d{2})\b", text)
if not match:
return None
try:
return date.fromisoformat(match.group(1))
except ValueError:
return None
def _parse_datetime(value: str) -> datetime | None:
if not value:
return None
normalized = value.replace("Z", "+00:00")
try:
parsed = datetime.fromisoformat(normalized)
except ValueError:
return None
if parsed.tzinfo is None:
return parsed.replace(tzinfo=ZoneInfo(APP_TIMEZONE))
return parsed
def _parse_time(value: str) -> time | None:
match = re.search(r"\b([01]?\d|2[0-3])(?::([0-5]\d))?\s*(am|pm)?\b", value, re.I)
if not match:
return None
hour = int(match.group(1))
minute = int(match.group(2) or 0)
meridiem = (match.group(3) or "").lower()
if meridiem == "pm" and hour < 12:
hour += 12
if meridiem == "am" and hour == 12:
hour = 0
return time(hour, minute)
|