Skip to content

Commit a6b5159

Browse files
committed
Fix all-day events entered in Google Cal - Reduce duplication with Gtasks
Fixes #126
1 parent 9f70769 commit a6b5159

File tree

6 files changed

+62
-123
lines changed

6 files changed

+62
-123
lines changed

syncall/google/common.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from __future__ import annotations
2+
3+
import datetime
4+
from typing import TYPE_CHECKING, cast
5+
6+
import dateutil
7+
from bubop import assume_local_tz_if_none
8+
9+
if TYPE_CHECKING:
10+
from syncall.types import GoogleDateT
11+
12+
13+
def parse_google_datetime(dt: GoogleDateT) -> datetime.datetime:
14+
"""Parse datetime given in format(s) returned by the Google API:
15+
- string with ('T', 'Z' separators).
16+
- (dateTime, timeZone) dictionary
17+
- datetime object
18+
19+
The output datetime is always in local timezone.
20+
"""
21+
if isinstance(dt, str):
22+
dt_dt = dateutil.parser.parse(dt) # type: ignore
23+
return parse_google_datetime(dt_dt)
24+
25+
if isinstance(dt, dict):
26+
for key in "dateTime", "date":
27+
if key in dt:
28+
date_time = cast(str, dt.get(key))
29+
break
30+
else:
31+
raise RuntimeError(f"Invalid structure dict: {dt}")
32+
33+
return parse_google_datetime(date_time)
34+
35+
if isinstance(dt, datetime.datetime):
36+
return assume_local_tz_if_none(dt)
37+
38+
raise TypeError(
39+
f"Unexpected type of a given date item, type: {type(dt)}, contents: {dt}",
40+
)

syncall/google/gcal_side.py

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,17 @@
22

33
import datetime
44
from pathlib import Path
5-
from typing import TYPE_CHECKING, Literal, Sequence, cast
5+
from typing import Literal, Sequence, cast
66

7-
import dateutil
87
import pkg_resources
9-
from bubop import assume_local_tz_if_none, format_datetime_tz, logger
8+
from bubop import logger
109
from googleapiclient import discovery
1110
from googleapiclient.http import HttpError
1211

12+
from syncall.google.common import parse_google_datetime
1313
from syncall.google.google_side import GoogleSide
1414
from syncall.sync_side import SyncSide
1515

16-
if TYPE_CHECKING:
17-
from syncall.types import GoogleDateT
18-
1916
DEFAULT_CLIENT_SECRET = pkg_resources.resource_filename(
2017
"syncall",
2118
"res/gcal_client_secret.json",
@@ -216,39 +213,7 @@ def get_event_time(item: dict, t: str) -> datetime.datetime:
216213
if isinstance(item[t], datetime.datetime):
217214
return item[t]
218215

219-
return GCalSide.parse_datetime(item[t][GCalSide.get_date_key(item[t])])
220-
221-
@staticmethod
222-
def format_datetime(dt: datetime.datetime) -> str:
223-
assert isinstance(dt, datetime.datetime)
224-
return format_datetime_tz(dt)
225-
226-
@classmethod
227-
def parse_datetime(cls, dt: GoogleDateT) -> datetime.datetime:
228-
"""Parse datetime given in the GCal format(s):
229-
- string with ('T', 'Z' separators).
230-
- (dateTime, dateZone) dictionary
231-
- datetime object
232-
233-
The output datetime is always in local timezone.
234-
"""
235-
if isinstance(dt, str):
236-
dt_dt = dateutil.parser.parse(dt) # type: ignore
237-
return cls.parse_datetime(dt_dt)
238-
239-
if isinstance(dt, dict):
240-
date_time = dt.get("dateTime")
241-
if date_time is None:
242-
raise RuntimeError(f"Invalid structure dict: {dt}")
243-
244-
return cls.parse_datetime(date_time)
245-
246-
if isinstance(dt, datetime.datetime):
247-
return assume_local_tz_if_none(dt)
248-
249-
raise TypeError(
250-
f"Unexpected type of a given date item, type: {type(dt)}, contents: {dt}",
251-
)
216+
return parse_google_datetime(item[t][GCalSide.get_date_key(item[t])])
252217

253218
@classmethod
254219
def items_are_identical(cls, item1, item2, ignore_keys: Sequence[str] = []) -> bool:
@@ -257,7 +222,7 @@ def items_are_identical(cls, item1, item2, ignore_keys: Sequence[str] = []) -> b
257222
if key not in item:
258223
continue
259224

260-
item[key] = cls.parse_datetime(item[key])
225+
item[key] = parse_google_datetime(item[key])
261226

262227
return SyncSide._items_are_identical(
263228
item1,

syncall/google/gtasks_side.py

Lines changed: 5 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
from pathlib import Path
55
from typing import TYPE_CHECKING, Sequence, cast
66

7-
import dateutil
87
import pkg_resources
9-
import pytz
10-
from bubop import format_datetime_tz, logger
8+
from bubop import logger
119
from googleapiclient import discovery
1210
from googleapiclient.http import HttpError
1311

1412
from syncall.google.google_side import GoogleSide
1513
from syncall.sync_side import SyncSide
1614

15+
from .common import parse_google_datetime
16+
1717
if TYPE_CHECKING:
1818
from syncall.types import GTasksItem, GTasksList
1919

@@ -229,7 +229,7 @@ def last_modification_key(cls) -> str:
229229
def _parse_dt_or_none(item: GTasksItem, field: str) -> datetime.datetime | None:
230230
"""Return the datetime on which task was completed in datetime format."""
231231
if (dt := item.get(field)) is not None:
232-
dt_dt = GTasksSide.parse_datetime(dt)
232+
dt_dt = parse_google_datetime(dt)
233233
assert isinstance(dt_dt, datetime.datetime)
234234
return dt_dt
235235

@@ -245,74 +245,14 @@ def get_task_completed_time(item: GTasksItem) -> datetime.datetime | None:
245245
"""Return the datetime on which task was completed in datetime format."""
246246
return GTasksSide._parse_dt_or_none(item=item, field="completed")
247247

248-
@staticmethod
249-
def format_datetime(dt: datetime.datetime) -> str:
250-
assert isinstance(dt, datetime.datetime)
251-
return format_datetime_tz(dt)
252-
253-
@classmethod
254-
def parse_datetime(cls, dt: str | dict | datetime.datetime) -> datetime.datetime:
255-
"""Parse datetime given in the GTasks format(s):
256-
- string with ('T', 'Z' separators).
257-
- (dateTime, dateZone) dictionary
258-
- datetime object
259-
260-
Usage::
261-
262-
>>> GTasksSide.parse_datetime("2019-03-05T00:03:09Z")
263-
datetime.datetime(2019, 3, 5, 0, 3, 9)
264-
>>> GTasksSide.parse_datetime("2019-03-05")
265-
datetime.datetime(2019, 3, 5, 0, 0)
266-
>>> GTasksSide.parse_datetime("2019-03-05T00:03:01.1234Z")
267-
datetime.datetime(2019, 3, 5, 0, 3, 1, 123400)
268-
>>> GTasksSide.parse_datetime("2019-03-08T00:29:06.602Z")
269-
datetime.datetime(2019, 3, 8, 0, 29, 6, 602000)
270-
271-
>>> from tzlocal import get_localzone_name
272-
>>> tz = get_localzone_name()
273-
>>> a = GTasksSide.parse_datetime({"dateTime": "2021-11-14T22:07:49Z", "timeZone": tz})
274-
>>> b = GTasksSide.parse_datetime({"dateTime": "2021-11-14T22:07:49.000000Z"})
275-
>>> b
276-
datetime.datetime(2021, 11, 14, 22, 7, 49)
277-
>>> from bubop.time import is_same_datetime
278-
>>> is_same_datetime(a, b) or (print(a) or print(b))
279-
True
280-
>>> GTasksSide.parse_datetime({"dateTime": "2021-11-14T22:07:49.123456"})
281-
datetime.datetime(2021, 11, 14, 22, 7, 49, 123456)
282-
>>> a = GTasksSide.parse_datetime({"dateTime": "2021-11-14T22:07:49Z", "timeZone": tz})
283-
>>> GTasksSide.parse_datetime(a).isoformat() == a.isoformat()
284-
True
285-
"""
286-
if isinstance(dt, str):
287-
return dateutil.parser.parse(dt).replace(tzinfo=None) # type: ignore
288-
289-
if isinstance(dt, dict):
290-
date_time = dt.get("dateTime")
291-
if date_time is None:
292-
raise RuntimeError(f"Invalid structure dict: {dt}")
293-
dt_dt = GTasksSide.parse_datetime(date_time)
294-
time_zone = dt.get("timeZone")
295-
if time_zone is not None:
296-
timezone = pytz.timezone(time_zone)
297-
dt_dt = timezone.localize(dt_dt)
298-
299-
return dt_dt
300-
301-
if isinstance(dt, datetime.datetime):
302-
return dt
303-
304-
raise TypeError(
305-
f"Unexpected type of a given date item, type: {type(dt)}, contents: {dt}",
306-
)
307-
308248
@classmethod
309249
def items_are_identical(cls, item1, item2, ignore_keys: Sequence[str] = []) -> bool:
310250
for item in [item1, item2]:
311251
for key in cls._date_keys:
312252
if key not in item:
313253
continue
314254

315-
item[key] = cls.parse_datetime(item[key])
255+
item[key] = parse_google_datetime(item[key])
316256

317257
return SyncSide._items_are_identical(
318258
item1,

syncall/tw_gcal_utils.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from datetime import timedelta
22
from uuid import UUID
33

4-
from bubop import logger
4+
from bubop import format_datetime_tz, logger
55
from item_synchronizer.types import Item
66

77
from syncall.google.gcal_side import GCalSide
@@ -84,11 +84,9 @@ def convert_tw_to_gcal(
8484
f'Using "{date_key}" date for {tw_item["uuid"]} for setting the end date of'
8585
" the event",
8686
)
87-
dt_gcal = GCalSide.format_datetime(tw_item[date_key])
87+
dt_gcal = format_datetime_tz(tw_item[date_key])
8888
gcal_item["start"] = {
89-
"dateTime": GCalSide.format_datetime(
90-
tw_item[date_key] - tw_item[tw_duration_key],
91-
),
89+
"dateTime": format_datetime_tz(tw_item[date_key] - tw_item[tw_duration_key]),
9290
}
9391
gcal_item["end"] = {"dateTime": dt_gcal}
9492
break
@@ -98,12 +96,12 @@ def convert_tw_to_gcal(
9896
" event",
9997
)
10098
entry_dt = tw_item["entry"]
101-
entry_dt_gcal_str = GCalSide.format_datetime(entry_dt)
99+
entry_dt_gcal_str = format_datetime_tz(entry_dt)
102100

103101
gcal_item["start"] = {"dateTime": entry_dt_gcal_str}
104102

105103
gcal_item["end"] = {
106-
"dateTime": GCalSide.format_datetime(entry_dt + tw_item[tw_duration_key]),
104+
"dateTime": format_datetime_tz(entry_dt + tw_item[tw_duration_key]),
107105
}
108106

109107
return gcal_item

syncall/tw_gtasks_utils.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from bubop import logger
1+
from bubop import format_datetime_tz, logger
22
from item_synchronizer.types import Item
33

4+
from syncall.google.common import parse_google_datetime
45
from syncall.google.gtasks_side import GTasksSide
56
from syncall.tw_utils import extract_tw_fields_from_string, get_tw_annotations_as_str
67
from syncall.types import GTasksItem
@@ -28,9 +29,7 @@ def convert_tw_to_gtask(
2829

2930
# update time
3031
if "modified" in tw_item.keys():
31-
gtasks_item["updated"] = GTasksSide.format_datetime(
32-
GTasksSide.parse_datetime(tw_item["modified"]),
33-
)
32+
gtasks_item["updated"] = format_datetime_tz(parse_google_datetime(tw_item["modified"]))
3433

3534
return gtasks_item
3635

@@ -92,6 +91,6 @@ def convert_gtask_to_tw(
9291

9392
# update time
9493
if "updated" in gtasks_item.keys():
95-
tw_item["modified"] = GTasksSide.parse_datetime(gtasks_item["updated"])
94+
tw_item["modified"] = parse_google_datetime(gtasks_item["updated"])
9695

9796
return tw_item

tests/test_gcal.py renamed to tests/test_google_common.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import datetime
22

3-
import syncall.google.gcal_side as side
43
from bubop import is_same_datetime
54
from dateutil.tz import gettz, tzutc
5+
from syncall.google import common
66
from syncall.types import GoogleDateT
77

88
localzone = gettz("Europe/Athens")
@@ -13,12 +13,9 @@ def assume_local_tz_if_none_(dt: datetime.datetime):
1313
return dt if dt.tzinfo is not None else dt.replace(tzinfo=localzone)
1414

1515

16-
side.assume_local_tz_if_none = assume_local_tz_if_none_
17-
18-
1916
def assert_dt(dt_given: GoogleDateT, dt_expected: datetime.datetime):
20-
parse_datetime = side.GCalSide.parse_datetime
21-
dt_dt_given = parse_datetime(dt_given)
17+
common.assume_local_tz_if_none = assume_local_tz_if_none_
18+
dt_dt_given = common.parse_google_datetime(dt_given)
2219

2320
# make sure there's always a timezone associated with this date
2421
assert dt_dt_given.tzinfo is not None

0 commit comments

Comments
 (0)