rename_source.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. #!/usr/bin/env python3
  2. """Rename a field in x_data.source for CalDAV events within a date range.
  3. Basic mode (no --domain/--attr): matches events where source.domain == old_key
  4. and rewrites source.domain to new_key. Identical to rename_domain.py.
  5. Extended mode (--domain and --attr both supplied): matches events where
  6. source.domain == DOMAIN and rewrites source.ATTR from old_key to new_key.
  7. --domain and --attr must be supplied together or not at all.
  8. Usage:
  9. python scripts/rename_source.py OLD_KEY NEW_KEY START_DATE END_DATE [--calendar NAME] [--dry-run]
  10. python scripts/rename_source.py OLD_KEY NEW_KEY START_DATE END_DATE --domain DOMAIN --attr ATTR [--calendar NAME] [--dry-run]
  11. Examples:
  12. # Rename source.domain from airbnb to vrbo
  13. python scripts/rename_source.py airbnb vrbo 2026-01-01 2026-12-31
  14. # For all events belonging to domain airbnb, rename source.id from abc to xyz
  15. python scripts/rename_source.py abc xyz 2026-01-01 2026-12-31 --domain airbnb --attr id
  16. """
  17. import argparse
  18. import asyncio
  19. import json
  20. import sys
  21. from datetime import date
  22. sys.path.insert(0, str(__import__('pathlib').Path(__file__).parent.parent))
  23. from room_cal.store import Calendar
  24. async def run(
  25. old_key: str,
  26. new_key: str,
  27. start: date,
  28. end: date,
  29. calendar: str,
  30. dry_run: bool,
  31. domain: str | None,
  32. attr: str,
  33. ) -> None:
  34. cal = Calendar(calendar)
  35. events = await cal.fetch_date_range(start, end)
  36. updated = 0
  37. for event in events:
  38. try:
  39. raw = json.loads(event['x_data'] or '{}')
  40. except (TypeError, json.JSONDecodeError):
  41. continue
  42. source = raw.get('source')
  43. if not isinstance(source, dict):
  44. continue
  45. if domain is None:
  46. # Basic mode: match and update source.domain
  47. if source.get('domain') != old_key:
  48. continue
  49. else:
  50. # Extended mode: match on source.domain, update source.<attr>
  51. if source.get('domain') != domain:
  52. continue
  53. if source.get(attr) != old_key:
  54. continue
  55. new_x_data = json.dumps({**raw, 'source': {**source, attr: new_key}})
  56. event_date = event['start'].date() if hasattr(event['start'], 'date') else event['start']
  57. print(f" {event_date} [{event['title']}]: source.{attr} {old_key!r} -> {new_key!r}", end='')
  58. if dry_run:
  59. print(' [dry-run, skipped]')
  60. else:
  61. await cal.update_event(event, x_data=new_x_data)
  62. print(' [updated]')
  63. updated += 1
  64. if updated == 0:
  65. if domain is None:
  66. print(f"No events with source.domain='{old_key}' found in {start} – {end}.")
  67. else:
  68. print(f"No events with source.domain='{domain}' and source.{attr}='{old_key}' found in {start} – {end}.")
  69. else:
  70. action = 'would update' if dry_run else 'updated'
  71. print(f"\n{action} {updated} event(s).")
  72. def main() -> None:
  73. parser = argparse.ArgumentParser(description='Rename a field in x_data.source for CalDAV events.')
  74. parser.add_argument('old_key', help='Current value to replace')
  75. parser.add_argument('new_key', help='Replacement value')
  76. parser.add_argument('start_date', help='Start date (YYYY-MM-DD, inclusive)')
  77. parser.add_argument('end_date', help='End date (YYYY-MM-DD, inclusive)')
  78. parser.add_argument('--domain', default=None, help='Filter by source.domain (required with --attr)')
  79. parser.add_argument('--attr', default=None, help='source field to update (default: domain)')
  80. parser.add_argument('--calendar', default='Room', help='Calendar name (default: Room)')
  81. parser.add_argument('--dry-run', action='store_true', help='Show what would change without writing')
  82. args = parser.parse_args()
  83. if (args.domain is None) != (args.attr is None):
  84. parser.error('--domain and --attr must be supplied together or not at all')
  85. start = date.fromisoformat(args.start_date)
  86. end = date.fromisoformat(args.end_date)
  87. # In basic mode attr is the field being matched and updated
  88. attr = args.attr if args.attr is not None else 'domain'
  89. asyncio.run(run(args.old_key, args.new_key, start, end, args.calendar, args.dry_run, args.domain, attr))
  90. if __name__ == '__main__':
  91. main()