From 51a6a8be83fd7e4dda1c3e4e3df4e62cd376ce33 Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Thu, 9 Apr 2026 16:26:17 +0200 Subject: [PATCH] more gcal fixes --- eventplanner_gcal/google_sync.py | 93 ++++++++++++++++++++++++-------- eventplanner_gcal/signals.py | 6 +-- 2 files changed, 73 insertions(+), 26 deletions(-) diff --git a/eventplanner_gcal/google_sync.py b/eventplanner_gcal/google_sync.py index 5763773..46f6e10 100644 --- a/eventplanner_gcal/google_sync.py +++ b/eventplanner_gcal/google_sync.py @@ -325,11 +325,49 @@ def delete_all_gcal_events(service=None): return count +def _execute_in_chunks(service, request_callback_pairs, chunk_size=30, delay=12): + """ + Execute API requests in small batches with a sleep between chunks. + + Google Calendar API allows 500 requests per 100 seconds per user (~5 req/s). + Default: 30 requests per chunk, 12 s sleep → ~2.5 req/s average. + + request_callback_pairs: list of (request, callback_or_None) + """ + total = len(request_callback_pairs) + for i in range(0, total, chunk_size): + chunk = request_callback_pairs[i : i + chunk_size] + batch = service.new_batch_http_request() + for req, cb in chunk: + if cb is not None: + batch.add(req, callback=cb) + else: + batch.add(req) + try: + batch.execute() + except Exception as e: + logger.error(f"Error executing batch chunk {i // chunk_size + 1}: {e}") + status = getattr(e, "status_code", None) or getattr(e, "resp", {}).get("status") + if str(status) in ("401", "403"): + _invalidate_service_on_error(e) + return # auth broken, no point continuing + + if i + chunk_size < total: + logger.info( + f"Chunk {i // chunk_size + 1} done " + f"({min(i + chunk_size, total)}/{total}), " + f"sleeping {delay}s to stay within rate limits..." + ) + time.sleep(delay) + + def sync_from_local_to_google(service=None): """ Creates a google event for each local event (if it does not exist yet) and deletes all google events that are not found in local database. Updates participation info of gcal events using local data. + + Creates are processed in chunks (future events first) to avoid rate limits. """ if service is None: service = get_service_object() @@ -375,26 +413,43 @@ def sync_from_local_to_google(service=None): if django_id not in local_events_django_id } - batch = service.new_batch_http_request() - batch_is_empty = True + # --- Deletes (usually few, single batch is fine) --- + if events_to_delete_google_id: + delete_pairs = [ + (service.events().delete(calendarId="primary", eventId=gcal_id), None) + for gcal_id in events_to_delete_google_id + ] + _execute_in_chunks(service, delete_pairs) - for event_django_id in events_to_create_django_id: + # --- Creates: future events first (soonest upcoming), then past events --- + today = datetime.date.today() + future_ids = list( + Event.objects.filter(pk__in=events_to_create_django_id, date__gte=today) + .order_by("date") + .values_list("pk", flat=True) + ) + past_ids = list( + Event.objects.filter(pk__in=events_to_create_django_id, date__lt=today) + .order_by("-date") + .values_list("pk", flat=True) + ) + ordered_create_ids = future_ids + past_ids + + create_pairs = [] + for event_django_id in ordered_create_ids: try: event = Event.objects.get(pk=event_django_id) - batch.add( - create_gcal_event_request(service, event), - callback=on_gcal_event_created, + create_pairs.append( + (create_gcal_event_request(service, event), on_gcal_event_created) ) - batch_is_empty = False except Event.DoesNotExist: pass - for event_google_id in events_to_delete_google_id: - batch.add( - service.events().delete(calendarId="primary", eventId=event_google_id) - ) - batch_is_empty = False + if create_pairs: + _execute_in_chunks(service, create_pairs) + # --- Updates: attendee status changes --- + update_pairs = [] for gcal_ev in all_events: try: event_django_id = int( @@ -405,23 +460,15 @@ def sync_from_local_to_google(service=None): gcal_attendees = gcal_ev.get("attendees", []) local_attendees = build_gcal_attendees_obj(django_ev) - # Simple comparison - check if attendees differ if gcal_attendees != local_attendees: - batch.add(update_gcal_event_request(service, django_ev)) - batch_is_empty = False + update_pairs.append((update_gcal_event_request(service, django_ev), None)) except Event.DoesNotExist: pass except (KeyError, ValueError): pass - if not batch_is_empty: - try: - batch.execute() - except Exception as e: - logger.error(f"Error executing batch request: {e}") - status = getattr(e, 'status_code', None) or getattr(e, 'resp', {}).get('status') - if str(status) in ('401', '403'): - _invalidate_service_on_error(e) + if update_pairs: + _execute_in_chunks(service, update_pairs) return len(events_to_create_django_id), len(events_to_delete_google_id) diff --git a/eventplanner_gcal/signals.py b/eventplanner_gcal/signals.py index 82716cb..abc6cb1 100644 --- a/eventplanner_gcal/signals.py +++ b/eventplanner_gcal/signals.py @@ -41,7 +41,7 @@ def event_post_save_handler(sender, instance, created, **kwargs): Currently disabled - remove the early return to enable. """ # Disabled - remove this return statement to enable auto-sync - return + #return event = instance service = get_service_object() @@ -73,7 +73,7 @@ def event_pre_delete_handler(sender, instance, **kwargs): Currently disabled - remove the early return to enable. """ # Disabled - remove this return statement to enable auto-sync - return + #return event = instance service = get_service_object() @@ -100,7 +100,7 @@ def participation_post_save_handler(sender, instance, **kwargs): Currently disabled - remove the early return to enable. """ # Disabled - remove this return statement to enable auto-sync - return + #return participation = instance service = get_service_object()