Skip to content

Commit 38899a2

Browse files
Merge pull request #1068 from closeio/migrate-to-ms-graph-event-ical-uid
Migrate MS Events to use `iCalUId` for events created after cutoff date
2 parents 69ea9ca + 5d0ff43 commit 38899a2

File tree

6 files changed

+473
-12
lines changed

6 files changed

+473
-12
lines changed

inbox/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import errno
22
import os
33

4+
import ciso8601
45
import urllib3
56
import yaml
67

@@ -124,6 +125,17 @@ def _update_config_from_env_variables( # type: ignore[no-untyped-def]
124125
)
125126
config["CALENDAR_POLL_FREQUENCY"] = calendar_poll_frequencey
126127

128+
# MS Calendar events created before this cutoff will continue to use the legacy
129+
# ID for all event association/deduplication. After the cutoff, the primary ID
130+
# will be taken from iCalUId, to allow deduplication of events shared between
131+
# calendars.
132+
ms_graph_ical_uid_cutoff_str = os.environ.get(
133+
"MS_GRAPH_ICAL_UID_CUTOFF", ""
134+
) or config.get("MS_GRAPH_ICAL_UID_CUTOFF", "2025-07-30T00:00:00Z")
135+
config["MS_GRAPH_ICAL_UID_CUTOFF"] = ciso8601.parse_datetime(
136+
ms_graph_ical_uid_cutoff_str
137+
)
138+
127139

128140
def _get_process_name(config) -> None: # type: ignore[no-untyped-def]
129141
if os.environ.get("PROCESS_NAME") is not None:

inbox/events/microsoft/events_provider.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
validate_event,
2424
)
2525
from inbox.events.util import CalendarSyncResponse
26+
from inbox.logging import get_logger
2627
from inbox.models.account import Account
2728
from inbox.models.backends.outlook import MICROSOFT_CALENDAR_SCOPES
2829
from inbox.models.calendar import Calendar
@@ -46,6 +47,8 @@
4647

4748
EVENT_FIELDS = [
4849
"id",
50+
"iCalUId",
51+
"createdDateTime",
4952
"type",
5053
"subject",
5154
"start",
@@ -154,27 +157,32 @@ def sync_events(
154157

155158
if isinstance(event, RecurringEvent):
156159
(exceptions, cancellations) = self._get_event_overrides(
157-
raw_event, event, read_only=read_only
160+
raw_event,
161+
event,
162+
ms_graph_event_id=raw_event["id"],
163+
read_only=read_only,
158164
)
159165
updates.extend(exceptions) # type: ignore[arg-type]
160166
updates.extend(cancellations) # type: ignore[arg-type]
161167

162168
return updates
163169

164-
def _get_event_overrides( # type: ignore[no-untyped-def]
170+
def _get_event_overrides(
165171
self,
166172
raw_master_event: MsGraphEvent,
167173
master_event: RecurringEvent,
168174
*,
169-
read_only,
175+
ms_graph_event_id: str,
176+
read_only: bool,
170177
) -> tuple[list[MsGraphEvent], list[MsGraphEvent]]:
171178
"""
172179
Fetch recurring event instances and determine exceptions and cancellations.
173180
174181
Arguments:
175-
raw_master_event: Recurring master event as retruend by the API
176-
master_event: Parsed recurring master event as ORM object
177-
read_only: Does master event come from read-only calendar
182+
raw_master_event: Recurring master event as returned by the API
183+
master_event: Parsed recurring master event as an ORM object
184+
ms_graph_event_id: Microsoft Graph event ID for use with Graph API
185+
read_only: Does master event come from a read-only calendar
178186
179187
Returns:
180188
Tuple of exceptions and cancellations
@@ -184,15 +192,28 @@ def _get_event_overrides( # type: ignore[no-untyped-def]
184192

185193
start = master_event.start
186194
end = start + MAX_RECURRING_EVENT_WINDOW
187-
195+
log = get_logger()
196+
log.info(
197+
"Getting overrides for event",
198+
event_uid=master_event.uid,
199+
event_ms_graph_id=ms_graph_event_id,
200+
)
188201
raw_occurrences = cast(
189202
list[MsGraphEvent],
190203
list(
191204
self.client.iter_event_instances(
192-
master_event.uid, start=start, end=end, fields=EVENT_FIELDS
205+
ms_graph_event_id,
206+
start=start,
207+
end=end,
208+
fields=EVENT_FIELDS,
193209
)
194210
),
195211
)
212+
log.info(
213+
"got overrides for event",
214+
event_id=master_event.uid,
215+
occurrence_count=len(raw_occurrences),
216+
)
196217
(raw_exceptions, raw_cancellations) = (
197218
calculate_exception_and_canceled_occurrences(
198219
raw_master_event, raw_occurrences, end

inbox/events/microsoft/graph_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,9 @@ class MsGraphEvent(TypedDict):
200200
"""
201201

202202
id: str
203+
iCalUId: str
203204
type: MsGraphEventType
205+
createdDateTime: str
204206
start: MsGraphDateTimeTimeZone
205207
originalStart: str
206208
end: MsGraphDateTimeTimeZone

inbox/events/microsoft/parse.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import pytz
1111
import pytz.tzinfo
1212

13+
from inbox.config import config
1314
from inbox.events.microsoft.graph_types import (
1415
ICalDayOfWeek,
1516
ICalFreq,
@@ -25,6 +26,7 @@
2526
MsGraphWeekIndex,
2627
)
2728
from inbox.events.timezones import windows_timezones
29+
from inbox.logging import get_logger
2830
from inbox.models.calendar import Calendar
2931
from inbox.models.event import Event
3032
from inbox.util.html import strip_tags
@@ -415,6 +417,11 @@ def synthesize_canceled_occurrence(
415417
+ "-synthesizedCancellation-"
416418
+ start_datetime.date().isoformat()
417419
)
420+
cancellation_ical_uid = (
421+
master_event["iCalUId"]
422+
+ "-synthesizedCancellation-"
423+
+ start_datetime.date().isoformat()
424+
)
418425
cancellation_start = dump_datetime_as_msgraph_datetime_tz(start_datetime)
419426
assert start_datetime.tzinfo == pytz.UTC
420427
original_start = start_datetime.replace(tzinfo=None).isoformat() + "Z"
@@ -425,9 +432,13 @@ def synthesize_canceled_occurrence(
425432
start_datetime + duration
426433
)
427434

435+
# As this is still an MSGraphEvent, we need to maintain separate id and
436+
# iCalUId. The decision for which to use will be made when parsing this into
437+
# a sync engine internal Event model.
428438
result = {
429439
**master_event,
430440
"id": cancellation_id,
441+
"iCalUId": cancellation_ical_uid,
431442
"type": "synthesizedCancellation",
432443
"isCancelled": True,
433444
"recurrence": None,
@@ -692,8 +703,19 @@ def parse_event(
692703
]:
693704
assert master_event_uid
694705
assert event["type"] in ["exception", "synthesizedCancellation"]
695-
696-
uid = event["id"]
706+
log = get_logger()
707+
log.info(
708+
"Parsing event",
709+
event_ical_id=event["iCalUId"],
710+
event_uid=event["id"],
711+
created_at=event["createdDateTime"],
712+
title=event["subject"],
713+
)
714+
ical_uid = event["iCalUId"]
715+
event_id = event["id"]
716+
created_date_time = datetime.datetime.fromisoformat(
717+
event["createdDateTime"]
718+
)
697719
raw_data = json.dumps(event)
698720
title = event["subject"] or ""
699721
start = parse_msgraph_datetime_tz_as_utc(event["start"])
@@ -744,7 +766,11 @@ def parse_event(
744766
original_start = None
745767

746768
return Event.create(
747-
uid=uid,
769+
uid=(
770+
ical_uid
771+
if created_date_time > config["MS_GRAPH_ICAL_UID_CUTOFF"]
772+
else event_id
773+
),
748774
raw_data=raw_data,
749775
title=title,
750776
description=description,

0 commit comments

Comments
 (0)