#!/usr/bin/env python3 """Rename a field in x_data.source for CalDAV events within a date range. Basic mode (no --domain/--attr): matches events where source.domain == old_key and rewrites source.domain to new_key. Identical to rename_domain.py. Extended mode (--domain and --attr both supplied): matches events where source.domain == DOMAIN and rewrites source.ATTR from old_key to new_key. --domain and --attr must be supplied together or not at all. Usage: python scripts/rename_source.py OLD_KEY NEW_KEY START_DATE END_DATE [--calendar NAME] [--dry-run] python scripts/rename_source.py OLD_KEY NEW_KEY START_DATE END_DATE --domain DOMAIN --attr ATTR [--calendar NAME] [--dry-run] Examples: # Rename source.domain from airbnb to vrbo python scripts/rename_source.py airbnb vrbo 2026-01-01 2026-12-31 # For all events belonging to domain airbnb, rename source.id from abc to xyz python scripts/rename_source.py abc xyz 2026-01-01 2026-12-31 --domain airbnb --attr id """ import argparse import asyncio import json import sys from datetime import date sys.path.insert(0, str(__import__('pathlib').Path(__file__).parent.parent)) from room_cal.cal import Calendar async def run( old_key: str, new_key: str, start: date, end: date, calendar: str, dry_run: bool, domain: str | None, attr: str, ) -> None: cal = Calendar(calendar) events = await cal.fetch_date_range(start, end) updated = 0 for event in events: try: raw = json.loads(event['x_data'] or '{}') except (TypeError, json.JSONDecodeError): continue source = raw.get('source') if not isinstance(source, dict): continue if domain is None: # Basic mode: match and update source.domain if source.get('domain') != old_key: continue else: # Extended mode: match on source.domain, update source. if source.get('domain') != domain: continue if source.get(attr) != old_key: continue new_x_data = json.dumps({**raw, 'source': {**source, attr: new_key}}) event_date = event['start'].date() if hasattr(event['start'], 'date') else event['start'] print(f" {event_date} [{event['title']}]: source.{attr} {old_key!r} -> {new_key!r}", end='') if dry_run: print(' [dry-run, skipped]') else: await cal.update_event(event, x_data=new_x_data) print(' [updated]') updated += 1 if updated == 0: if domain is None: print(f"No events with source.domain='{old_key}' found in {start} – {end}.") else: print(f"No events with source.domain='{domain}' and source.{attr}='{old_key}' found in {start} – {end}.") else: action = 'would update' if dry_run else 'updated' print(f"\n{action} {updated} event(s).") def main() -> None: parser = argparse.ArgumentParser(description='Rename a field in x_data.source for CalDAV events.') parser.add_argument('old_key', help='Current value to replace') parser.add_argument('new_key', help='Replacement value') parser.add_argument('start_date', help='Start date (YYYY-MM-DD, inclusive)') parser.add_argument('end_date', help='End date (YYYY-MM-DD, inclusive)') parser.add_argument('--domain', default=None, help='Filter by source.domain (required with --attr)') parser.add_argument('--attr', default=None, help='source field to update (default: domain)') parser.add_argument('--calendar', default='Room', help='Calendar name (default: Room)') parser.add_argument('--dry-run', action='store_true', help='Show what would change without writing') args = parser.parse_args() if (args.domain is None) != (args.attr is None): parser.error('--domain and --attr must be supplied together or not at all') start = date.fromisoformat(args.start_date) end = date.fromisoformat(args.end_date) # In basic mode attr is the field being matched and updated attr = args.attr if args.attr is not None else 'domain' asyncio.run(run(args.old_key, args.new_key, start, end, args.calendar, args.dry_run, args.domain, attr)) if __name__ == '__main__': main()