booking configuration validation

smtp-email-notifications-attempt
Ibraheem Saleh 8 months ago
parent 00ae8bc60d
commit aa4b0bc86b

@ -106,18 +106,26 @@ List of available courts:
``` ```
### Reservation Behaviors ### Reservation Behaviors
Defines how reservations are attempted: Defines how reservations are attempted. **Important: Times must be in ascending order.**
```json ```json
{ {
"Name": "Weekday_Reservation_Type", "Name": "Weekday_Reservation_Type",
"DaysInAdvance": 7, "DaysInAdvance": 7,
"AttemptSchedule": [ "AttemptSchedule": [
{"Time": "04:29:00-08:00", "Attempts": 2, "DelaySeconds": 10}, {"Time": "04:29:00-08:00", "Attempts": 2, "DelaySeconds": 10},
{"Time": "04:30:00-08:00", "Attempts": 10, "DelaySeconds": 10} {"Time": "04:30:00-08:00", "Attempts": 10, "DelaySeconds": 10},
{"Time": "04:35:00-08:00", "Attempts": 1, "DelaySeconds": 0}
] ]
} }
``` ```
- `DaysInAdvance`: How many days before the reservation date to attempt booking
- `AttemptSchedule`: List of attempt windows (must be in chronological order)
- `Time`: When to start attempting (in timezone format like "04:30:00-08:00")
- `Attempts`: Number of booking attempts in this window
- `DelaySeconds`: Delay between attempts
### Reservations ### Reservations
Scheduled reservation configurations: Scheduled reservation configurations:
```json ```json

@ -38,6 +38,17 @@
{"Time": "04:35:00-08:00", "Attempts": 1, "DelaySeconds": 0} {"Time": "04:35:00-08:00", "Attempts": 1, "DelaySeconds": 0}
] ]
}, },
{
"Name": "Test_Reservation_Type",
"DaysInAdvance": 7,
"AttemptSchedule": [
{"Time": "04:29:00-08:00", "Attempts": 2, "DelaySeconds": 10},
{"Time": "04:31:00-08:00", "Attempts": 2, "DelaySeconds": 10},
{"Time": "04:35:00-08:00", "Attempts": 1, "DelaySeconds": 0},
{"Time": "20:05:00-08:00", "Attempts": 4, "DelaySeconds": 10},
{"Time": "20:11:00-08:00", "Attempts": 4, "DelaySeconds": 10}
]
},
{ {
"Name": "Weekend_Reservation_Type", "Name": "Weekend_Reservation_Type",
"DaysInAdvance": 7, "DaysInAdvance": 7,
@ -110,7 +121,7 @@
{ {
"Day": "Sunday", "Day": "Sunday",
"Time": "15:30:00-08:00", "Time": "15:30:00-08:00",
"ReservationBehavior": "Weekday_Reservation_Type", "ReservationBehavior": "Test_Reservation_Type",
"Courts": [ "Courts": [
{"Court": "Court_3", "Account": "Saleh"} {"Court": "Court_3", "Account": "Saleh"}
] ]

@ -172,10 +172,49 @@ def load_config(config_path: str = "config.json") -> Config:
if "Name" not in beh or "DaysInAdvance" not in beh or "AttemptSchedule" not in beh: if "Name" not in beh or "DaysInAdvance" not in beh or "AttemptSchedule" not in beh:
raise ValueError(f"Invalid reservation behavior: {beh}") raise ValueError(f"Invalid reservation behavior: {beh}")
if not beh["AttemptSchedule"]:
raise ValueError(f"ReservationBehavior '{beh['Name']}' must have at least one AttemptSchedule entry")
schedules = [] schedules = []
for sched in beh["AttemptSchedule"]: prev_time_minutes = -1
for i, sched in enumerate(beh["AttemptSchedule"]):
if "Time" not in sched or "Attempts" not in sched or "DelaySeconds" not in sched: if "Time" not in sched or "Attempts" not in sched or "DelaySeconds" not in sched:
raise ValueError(f"Invalid attempt schedule: {sched}") raise ValueError(f"Invalid attempt schedule in '{beh['Name']}': {sched}")
# Validate time format and check ascending order
time_str = sched["Time"]
try:
# Parse time "HH:MM:SS-TZ:00" format
if len(time_str) < 8:
raise ValueError(f"Invalid time format")
time_part = time_str[:8]
parts = time_part.split(':')
if len(parts) != 3:
raise ValueError(f"Invalid time format")
hour = int(parts[0])
minute = int(parts[1])
second = int(parts[2])
if not (0 <= hour <= 23 and 0 <= minute <= 59 and 0 <= second <= 59):
raise ValueError(f"Invalid time values")
# Convert to minutes for comparison
current_time_minutes = hour * 60 + minute
if current_time_minutes <= prev_time_minutes:
raise ValueError(
f"AttemptSchedule times in '{beh['Name']}' must be in ascending order. "
f"Entry {i+1} time '{time_str}' is not after previous entry. "
f"Please sort AttemptSchedule entries by time."
)
prev_time_minutes = current_time_minutes
except ValueError as e:
if "ascending order" in str(e) or "AttemptSchedule" in str(e):
raise
raise ValueError(f"Invalid time format '{time_str}' in '{beh['Name']}': {e}")
schedules.append(AttemptSchedule( schedules.append(AttemptSchedule(
time=sched["Time"], time=sched["Time"],
attempts=sched["Attempts"], attempts=sched["Attempts"],
@ -681,21 +720,85 @@ class HarborBot:
print("[ERROR] Failed to authenticate. Exiting.") print("[ERROR] Failed to authenticate. Exiting.")
return return
# Print initial status showing next scheduled attempts
self._print_upcoming_reservations()
last_status_print = datetime.now()
while True: while True:
try: try:
self._daemon_iteration(dry_run) self._daemon_iteration(dry_run)
# Sleep until next check (check every minute) # Print status every 30 minutes
time.sleep(60) if (datetime.now() - last_status_print).total_seconds() > 1800:
self._print_upcoming_reservations()
last_status_print = datetime.now()
# Sleep until next check (check every 30 seconds for better timing accuracy)
time.sleep(30)
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n[INFO] Daemon stopped by user") print("\n[INFO] Daemon stopped by user")
break break
except Exception as e: except Exception as e:
print(f"[ERROR] Daemon error: {e}") print(f"[ERROR] Daemon error: {e}")
import traceback
traceback.print_exc()
print("[INFO] Sleeping 60s before retry...") print("[INFO] Sleeping 60s before retry...")
time.sleep(60) time.sleep(60)
def _print_upcoming_reservations(self):
"""Print status of upcoming reservations."""
print(f"\n[STATUS] {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Checking upcoming reservations:")
upcoming = []
for reservation in self.config.reservations:
behavior = self.config.reservation_behaviors[reservation.reservation_behavior]
target_date = self.calculate_reservation_date(reservation.day, behavior.days_in_advance)
if not target_date:
continue
# Get timezone from reservation time
_, _, _, tz_offset = self._parse_schedule_time(reservation.time)
now = self._get_current_time_in_tz(tz_offset)
today = now.date()
# Find next upcoming attempt window
next_attempt_time = None
for schedule in behavior.attempt_schedule:
sched_time = self._get_schedule_datetime(schedule.time, datetime.combine(today, datetime.min.time()))
if sched_time > now:
next_attempt_time = sched_time
break
if next_attempt_time:
wait_seconds = (next_attempt_time - now).total_seconds()
upcoming.append({
'reservation': reservation,
'target_date': target_date,
'next_attempt': next_attempt_time,
'wait_seconds': wait_seconds
})
if not upcoming:
print(" No reservations scheduled for today")
else:
# Sort by next attempt time
upcoming.sort(key=lambda x: x['next_attempt'])
for item in upcoming:
res = item['reservation']
wait = item['wait_seconds']
if wait < 60:
wait_str = f"{wait:.0f}s"
elif wait < 3600:
wait_str = f"{wait/60:.1f}m"
else:
wait_str = f"{wait/3600:.1f}h"
print(f" - {res.day} @ {res.time} -> {item['target_date'].strftime('%Y-%m-%d')}: "
f"Next attempt at {item['next_attempt'].strftime('%H:%M:%S')} (in {wait_str})")
def _daemon_iteration(self, dry_run: bool): def _daemon_iteration(self, dry_run: bool):
"""Single iteration of the daemon loop.""" """Single iteration of the daemon loop."""
# Re-check authentication periodically # Re-check authentication periodically
@ -720,31 +823,64 @@ class HarborBot:
now = self._get_current_time_in_tz(tz_offset) now = self._get_current_time_in_tz(tz_offset)
today = now.date() today = now.date()
# Get first and last attempt times # Find if we're within any attempt window (within 60 seconds of a scheduled time)
first_schedule = behavior.attempt_schedule[0] for schedule in behavior.attempt_schedule:
last_schedule = behavior.attempt_schedule[-1] sched_time = self._get_schedule_datetime(schedule.time, datetime.combine(today, datetime.min.time()))
time_diff = (now - sched_time).total_seconds()
first_time = self._get_schedule_datetime(first_schedule.time, datetime.combine(today, datetime.min.time()))
last_time = self._get_schedule_datetime(last_schedule.time, datetime.combine(today, datetime.min.time())) # Trigger if we're within 0-60 seconds after the scheduled time
if 0 <= time_diff <= 60:
# Add buffer for last attempt window (assume max 10 minutes for attempts) print(f"\n[TRIGGER] {reservation.day} @ {reservation.time} for {target_date.strftime('%Y-%m-%d')}")
last_time_with_buffer = last_time + timedelta(minutes=10) print(f"[INFO] Attempt window: {schedule.time} (triggered at {now.strftime('%H:%M:%S')})")
self._execute_single_attempt_window(reservation, schedule, target_date, dry_run)
# Check if we're before the first window (wait) or past the last window (skip) break # Only execute once per iteration
if now < first_time - timedelta(minutes=5):
# More than 5 minutes before first attempt - not time yet def _execute_single_attempt_window(
wait_seconds = (first_time - now).total_seconds() self,
if wait_seconds < 300: # Less than 5 minutes away reservation: Reservation,
print(f"\n[INFO] {reservation.day} @ {reservation.time}: Starting in {wait_seconds:.0f}s") schedule: AttemptSchedule,
continue target_date: datetime,
dry_run: bool
):
"""Execute attempts for a single attempt window."""
reservation_datetime = target_date.strftime(f"%Y-%m-%dT{reservation.time[:8]}") + reservation.time[8:]
reservation_date_str = target_date.strftime("%Y-%m-%d")
print(f"[INFO] Booking for: {reservation_datetime}")
print(f"[INFO] Attempts: {schedule.attempts}, Delay: {schedule.delay_seconds}s")
for court_res in reservation.courts:
court = self.config.courts[court_res.court]
account = self.config.accounts[court_res.account]
if now > last_time_with_buffer: # Check if already successful
# Past all attempt windows for today if self._is_reservation_successful(reservation_date_str, court_res.court, court_res.account):
print(f" [SKIP] {court.name} for {account.name} - already booked successfully")
continue continue
# We're within the attempt window - execute for attempt in range(schedule.attempts):
print(f"\n[TRIGGER] {reservation.day} @ {reservation.time} for {target_date.strftime('%Y-%m-%d')}") if dry_run:
self.execute_reservation_attempts(reservation, behavior, target_date, dry_run) print(f" [DRY RUN] Attempt {attempt + 1}/{schedule.attempts}: "
f"Would book {court.name} for {account.name}")
else:
print(f" [ATTEMPT] {attempt + 1}/{schedule.attempts}: "
f"Booking {court.name} for {account.name}...")
result = self.book_appointment(court, account, reservation_datetime)
if result.get("Success"):
print(f" [SUCCESS] Booked {court.name} for {account.name}")
self._mark_reservation_successful(reservation_date_str, court_res.court, court_res.account)
break
else:
print(f" [FAILED] {result.get('Message', 'Unknown error')}")
if result.get("AllowRetry") is False and "no longer available" in result.get("Message", ""):
print(f" [INFO] Slot no longer available, moving to next court")
break
if attempt < schedule.attempts - 1 and schedule.delay_seconds > 0:
if not dry_run:
time.sleep(schedule.delay_seconds)
# After executing, we'll skip this reservation until tomorrow # After executing, we'll skip this reservation until tomorrow
# (the success tracking prevents re-booking, and time checks prevent re-triggering) # (the success tracking prevents re-booking, and time checks prevent re-triggering)

@ -61,11 +61,15 @@ curl 'https://future.ourclublogin.com/api/CustomerAuth/CustomerLogin' \
** We need to add behavior so that if a reservation succeeds, future attempts for that configuration do not try again. ** We need to add behavior so that if a reservation succeeds, future attempts for that configuration do not try again.
* Prompt 2: ##
* I tried to book a court for 7 days in advance with a weekend reservation type, and it booked the court right away, instead of waiting for the specified booking times. The bot should only attempt to make the reservations in live mode at the time of the reservation. * I tried to book a court for 7 days in advance with a weekend reservation type, and it booked the court right away, instead of waiting for the specified booking times. The bot should only attempt to make the reservations in live mode at the time of the reservation.
* Instead of having to start the bot every day, update it to just run continuously. If it every crashes, the service will just auto-restart... No reason to require 7 cron jobs--1 for every day. * Instead of having to start the bot every day, update it to just run continuously. If it every crashes, the service will just auto-restart... No reason to require 7 cron jobs--1 for every day.
##
* Add startup configuration validation to ensure that the AttemptSchedule is configured properly, with scheduled times incrementing as required by the algorithm.
##
* This will run on a raspberry pi on linux, implement a pure python method for sending an email everytime a court is successfully booked. Be sure to include what court was booked, what time it was booked for, and the user name and ID that the booking was creating under.

Loading…
Cancel
Save