patch_reservation_codes.py 3.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. #!/usr/bin/env python3
  2. """Patch wifi_key, wifi_ssid, and/or door_code on a reservation that contains a given date.
  3. Usage:
  4. uv run python3 scripts/patch_reservation_codes.py 2026-07-05 --wifi stone-angel-market --door 0472
  5. uv run python3 scripts/patch_reservation_codes.py 2026-07-05 --wifi new-word-combo --ssid MyNetwork
  6. uv run python3 scripts/patch_reservation_codes.py 2026-07-05 --door 1234
  7. """
  8. import argparse
  9. import asyncio
  10. import json
  11. import sys
  12. from datetime import date
  13. from room_cal.store import Calendar, DEFAULT_CALENDAR_NAME
  14. from room_cal._utils import safe_json_loads, filter_events, service_name
  15. _PREFIX = 'Guest: '
  16. def _build_description(xd: dict, wk: str, ws: str | None, dc: str) -> str:
  17. svc = xd.get('service', '')
  18. wifi_line = f"WiFi: {ws} / {wk}" if ws else f"WiFi: {wk}"
  19. return (
  20. f"Name: {xd.get('guest_name', '')}\n"
  21. f"Service: {service_name(svc)}\n"
  22. f"Reservation ID: {xd.get('reservation_id', '')}\n"
  23. f"Door: {dc}\n"
  24. f"{wifi_line}"
  25. )
  26. async def patch(target: date, new_wifi: str | None, new_ssid: str | None, new_door: str | None, calendar_name: str) -> None:
  27. cal = Calendar(calendar_name)
  28. events = await cal.fetch_date_range(target, target)
  29. reservations = filter_events(events, _PREFIX, all_day=False)
  30. if not reservations:
  31. print(f"No reservation found overlapping {target}", file=sys.stderr)
  32. sys.exit(1)
  33. if len(reservations) > 1:
  34. reservations.sort(key=lambda e: e['start'])
  35. event = reservations[-1]
  36. print(f"Multiple reservations overlap {target}, using the later one ({event['start'].date()} – {event['end'].date()})")
  37. else:
  38. event = reservations[0]
  39. xd = safe_json_loads(event['x_data']) or {}
  40. old_wifi = xd.get('wifi_key', '(none)')
  41. old_ssid = xd.get('wifi_ssid')
  42. old_door = xd.get('door_code', '(none)')
  43. wk = new_wifi if new_wifi is not None else old_wifi
  44. ws = new_ssid if new_ssid is not None else old_ssid
  45. dc = new_door if new_door is not None else old_door
  46. print(f"Reservation: {event['title']} ({event['start'].date()} – {event['end'].date()})")
  47. if new_wifi is not None:
  48. print(f" wifi_key: {old_wifi!r} → {wk!r}")
  49. if new_ssid is not None:
  50. print(f" wifi_ssid: {old_ssid!r} → {ws!r}")
  51. if new_door is not None:
  52. print(f" door_code: {old_door!r} → {dc!r}")
  53. confirm = input("Apply? [y/N] ").strip().lower()
  54. if confirm != 'y':
  55. print("Aborted.")
  56. return
  57. xd['wifi_key'] = wk
  58. xd['wifi_ssid'] = ws
  59. xd['door_code'] = dc
  60. new_x_data = json.dumps(xd)
  61. new_description = _build_description(xd, wk, ws, dc)
  62. await cal.update_event(event, description=new_description, x_data=new_x_data)
  63. print("Updated.")
  64. def main():
  65. parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
  66. parser.add_argument('date', type=date.fromisoformat, help='Any date within the reservation (YYYY-MM-DD)')
  67. parser.add_argument('--wifi', metavar='WIFI_KEY', help='New wifi_key string')
  68. parser.add_argument('--ssid', metavar='WIFI_SSID', help='New wifi_ssid string')
  69. parser.add_argument('--door', metavar='DOOR_CODE', help='New door_code string')
  70. parser.add_argument('--calendar', default=DEFAULT_CALENDAR_NAME, help=f'Calendar name (default: {DEFAULT_CALENDAR_NAME})')
  71. args = parser.parse_args()
  72. if args.wifi is None and args.ssid is None and args.door is None:
  73. parser.error('Provide at least one of --wifi, --ssid, or --door')
  74. asyncio.run(patch(args.date, args.wifi, args.ssid, args.door, args.calendar))
  75. if __name__ == '__main__':
  76. main()