TL;DR: I can reproduce a sequence of creating → updating → deleting → creating a Google Calendar entry via the API, and the last "creating" fails because of a "duplicate identified". Since the event with that identifier was deleted, I do not understand why reusing the ID raises an error. Is this a bug?
Consider the following code fragment (explanation below the code):
import googleapiclient.discovery
import google.oauth2
from loguru import logger as log
import arrow
# ID of the Google Calendar
CALENDAR_ID = "YYY@group.calendar.google.com"
# prepare session in Google Calendar
# see:
# - https://developers.google.com/identity/protocols/oauth2/service-account
# - https://developers.google.com/calendar/api/v3/reference/events/update
# - https://developers.google.com/calendar/api/guides/create-events#python
# - https://developers.google.com/calendar/api/v3/reference/events
creds = google.oauth2.service_account.Credentials.from_service_account_file(
"creds.json", scopes=["https://www.googleapis.com/auth/calendar"]
)
delegated_credentials = creds.with_subject("XXX")
service = googleapiclient.discovery.build("calendar", "v3", credentials=delegated_credentials)
# get all existing events from the calendar from today on
existing_lessons_in_calendar = service.events().list(calendarId=CALENDAR_ID, timeMin=arrow.now().floor('day')).execute()
# extract the actual lessons
existing_lessons_in_calendar = [lesson for lesson in existing_lessons_in_calendar["items"]]
# create a bag for the ids of the above
all_ids_from_calendar = [l["id"].lower() for l in existing_lessons_in_calendar]
log.debug(all_ids_from_calendar)
# create or update an entry in calendar from promote
# for SO: imagine we have a proper list of objects
for lesson in lessons_from_pronote:
# lesson_id = lesson.id.lower()
lesson_id = 'aaaaaa' # note
entry = {
"id": lesson_id,
"summary": lesson.subject.name,
"start": {
"dateTime": arrow.get(lesson.start, 'Europe/Paris').isoformat(),
},
"end": {
"dateTime": arrow.get(lesson.end, 'Europe/Paris').isoformat(),
},
}
log.debug(entry)
# check if the entry already exists in the calendar
if lesson_id in all_ids_from_calendar:
log.debug(f"UPDATING {entry['summary']} on {entry['start']['dateTime']}")
service.events().update(
calendarId=CALENDAR_ID,
eventId=lesson_id,
body=entry,
).execute()
else:
log.debug(f"CREATING {entry['summary']} on {entry['start']['dateTime']}")
service.events().insert(
calendarId=CALENDAR_ID,
body=entry,
).execute()
What it does is to either create (.insert()
) or update (.update()
) an event in the calendar, depending on whether it is already present or not. To this, I check if the id of the event I want to add is one of the existing events.
When running this code with a fresh ID (aaaaaa
in the case above) I get:
2022-09-11 13:04:29.292 | DEBUG | __main__:<module>:64 - []
2022-09-11 13:04:29.417 | DEBUG | __main__:<module>:80 - {'id': 'aaaaaa', 'summary': 'ENSEIGN.SCIENTIFIQUE', 'start': {'dateTime': '2022-09-13T10:05:00+02:00'}, 'end':
{'dateTime': '2022-09-13T10:55:00+02:00'}}
2022-09-11 13:04:29.419 | DEBUG | __main__:<module>:89 - CREATING ENSEIGN.SCIENTIFIQUE on 2022-09-13T10:05:00+02:00
Traceback (most recent call last):
File "d:\W\dev-perso\pronote\pronote.py", line 93, in <module>
).execute()
File "C:\Python310\lib\site-packages\googleapiclient\_helpers.py", line 130, in positional_wrapper
return wrapped(*args, **kwargs)
File "C:\Python310\lib\site-packages\googleapiclient\http.py", line 938, in execute
raise HttpError(resp, content, uri=self.uri)
googleapiclient.errors.HttpError: <HttpError 409 when requesting https://www.googleapis.com/calendar/v3/calendars/YYY%40group.calendar.google.com/events?alt=json returned "The requested identifier already exists.". Details: "[{'domain': 'global', 'reason': 'duplicate', 'message': 'The requested identifier already exists.'}]">
As you can see in the first line of the log, after the event has been deleted via the web interface, the list of existing events in the calendar is empty.
Despite this, the creation I rejected because the Id allegedly already exists.
This looks like a bug to me but I wanted to make sure I do not make a mistake somewhere. The documentation is quite clear about the nature of this identifier:
the ID must be unique per calendar
Note: I initially thought that this was some kind of "eventual consistency" on Google side, where the deletion would not have had the time to correctly replicate on all servers. 24 hours later, the problem persists.
The event on Google calendar has status
. The official document says it as follows.
- "confirmed" - The event is confirmed. This is the default status.
- "tentative" - The event is tentatively confirmed.
- "cancelled" - The event is cancelled (deleted). The list method returns cancelled events only on incremental sync (when syncToken or updatedMin are specified) or if the showDeleted flag is set to true. The get method always returns them.
For example, when a new event is created, the status is confirmed
. And, when the event is deleted, the status is changed to cancelled
. Under this condition, when a new event is created using the event id of deleted event, such an error like The requested identifier already exists.
occurs. From your showing script and your flow for replicating your issue, I thought that in this case, the reason for your current issue might be due to this.
When you want to avoid the current issue of The requested identifier already exists.
, how about the following modification?
If you want to check whether the event is deleted using the event ID, how about the following modification?
existing_lessons_in_calendar = service.events().list(calendarId=CALENDAR_ID, timeMin=arrow.now().floor('day')).execute()
existing_lessons_in_calendar = service.events().list(calendarId=CALENDAR_ID, timeMin=arrow.now().floor('day'), showDeleted=True).execute()
By this modification, the deleted event IDs are included. So, such an error doesn't occur. In this case, for example, when the event of event ID aaaaaa
has already been deleted, in your current script, the event is updated.
If you want to separate the existing events and the deleted events, how about the following modification?
# get all existing events from the calendar from today on
existing_lessons_in_calendar = service.events().list(calendarId=CALENDAR_ID, timeMin=arrow.now().floor('day'), showDeleted=True).execute()
obj = {"confirmed": [], "cancelled": []}
for e in existing_lessons_in_calendar["items"]:
if e["status"] == "confirmed":
obj["confirmed"].append(e["id"])
elif e["status"] == "cancelled":
obj["cancelled"].append(e["id"])
# create or update an entry in calendar from promote
# for SO: imagine we have a proper list of objects
for lesson in lessons_from_pronote:
# lesson_id = lesson.id.lower()
lesson_id = 'aaaaaa' # note
entry = {
"id": lesson_id,
"summary": lesson.subject.name,
"start": {
"dateTime": arrow.get(lesson.start, 'Europe/Paris').isoformat(),
},
"end": {
"dateTime": arrow.get(lesson.end, 'Europe/Paris').isoformat(),
},
}
log.debug(entry)
# check if the entry already exists in the calendar
if lesson_id in obj["confirmed"]:
log.debug(f"UPDATING {entry['summary']} on {entry['start']['dateTime']}")
service.events().update(
calendarId=CALENDAR_ID,
eventId=lesson_id,
body=entry,
).execute()
elif lesson_id in obj["cancelled"]:
# Please set the request body when the deleted event is used.
log.debug(f"UPDATING {entry['summary']} on {entry['start']['dateTime']}")
service.events().update(
calendarId=CALENDAR_ID,
eventId=lesson_id,
body=entry,
else:
log.debug(f"CREATING {entry['summary']} on {entry['start']['dateTime']}")
service.events().insert(
calendarId=CALENDAR_ID,
body=entry,
).execute()
By this modification, the script can be separated into the existing events, the deleted events, and not existing events.