Skip to content

Commit

Permalink
Fix all-day events entered in Google Cal - Reduce duplication with Gt…
Browse files Browse the repository at this point in the history
…asks
  • Loading branch information
bergercookie committed Aug 15, 2024
1 parent edfbaf6 commit 3d41b14
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 117 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ exclude = []
# No need for -> None in __init__ methods
mypy-init-return = true


# coverage.py ------------------------------------------------------------------
[tool.coverage]
[tool.coverage.run]
Expand Down
72 changes: 72 additions & 0 deletions syncall/google/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from __future__ import annotations

import datetime
from typing import TYPE_CHECKING, cast

import dateutil
import pytz
from bubop import assume_local_tz_if_none

if TYPE_CHECKING:
from syncall.types import GoogleDateT


def parse_google_datetime(dt: GoogleDateT) -> datetime.datetime:
"""Parse datetime given in format(s) returned by the Google API:
- string with ('T', 'Z' separators).
- (dateTime, timeZone) dictionary
- datetime object
The output datetime is always in local timezone.
Usage::
>>> parse_google_datetime("2019-03-05T00:03:09Z")
datetime.datetime(2019, 3, 5, 0, 3, 9)
>>> parse_google_datetime("2019-03-05")
datetime.datetime(2019, 3, 5, 0, 0)
>>> parse_google_datetime("2019-03-05T00:03:01.1234Z")
datetime.datetime(2019, 3, 5, 0, 3, 1, 123400)
>>> parse_google_datetime("2019-03-08T00:29:06.602Z")
datetime.datetime(2019, 3, 8, 0, 29, 6, 602000)
>>> from tzlocal import get_localzone_name
>>> tz = get_localzone_name()
>>> a = parse_google_datetime({"dateTime": "2021-11-14T22:07:49Z", "timeZone": tz})
>>> b = parse_google_datetime({"dateTime": "2021-11-14T22:07:49.000000Z"})
>>> b
datetime.datetime(2021, 11, 14, 22, 7, 49)
>>> from bubop.time import is_same_datetime
>>> is_same_datetime(a, b) or (print(a) or print(b))
True
>>> parse_google_datetime({"dateTime": "2021-11-14T22:07:49.123456"})
datetime.datetime(2021, 11, 14, 22, 7, 49, 123456)
>>> a = parse_google_datetime({"dateTime": "2021-11-14T22:07:49Z", "timeZone": tz})
>>> parse_google_datetime(a).isoformat() == a.isoformat()
True
"""
if isinstance(dt, str):
return dateutil.parser.parse(dt).replace(tzinfo=None) # type: ignore

if isinstance(dt, dict):
for key in "dateTime", "date":
if key in dt:
date_time = cast(str, dt.get(key))
break
else:
raise RuntimeError(f"Invalid structure dict: {dt}")

dt_dt = parse_google_datetime(date_time)
time_zone = dt.get("timeZone")
if time_zone is not None:
timezone = pytz.timezone(time_zone)
dt_dt = timezone.localize(dt_dt)

return dt_dt

if isinstance(dt, datetime.datetime):
return assume_local_tz_if_none(dt)

raise TypeError(
f"Unexpected type of a given date item, type: {type(dt)}, contents: {dt}",
)
43 changes: 2 additions & 41 deletions syncall/google/gcal_side.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,16 @@

import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Literal, Sequence, cast
from typing import Literal, Sequence, cast

import dateutil
import pkg_resources
from bubop import assume_local_tz_if_none, format_datetime_tz, logger
from bubop import logger
from googleapiclient import discovery
from googleapiclient.http import HttpError

from syncall.google.google_side import GoogleSide
from syncall.sync_side import SyncSide

if TYPE_CHECKING:
from syncall.types import GoogleDateT

DEFAULT_CLIENT_SECRET = pkg_resources.resource_filename(
"syncall",
"res/gcal_client_secret.json",
Expand Down Expand Up @@ -218,41 +214,6 @@ def get_event_time(item: dict, t: str) -> datetime.datetime:

return GCalSide.parse_datetime(item[t][GCalSide.get_date_key(item[t])])

@staticmethod
def format_datetime(dt: datetime.datetime) -> str:
assert isinstance(dt, datetime.datetime)
return format_datetime_tz(dt)

@classmethod
def parse_datetime(cls, dt: GoogleDateT) -> datetime.datetime:
"""Parse datetime given in the GCal format(s):
- string with ('T', 'Z' separators).
- (dateTime, dateZone) dictionary
- datetime object
The output datetime is always in local timezone.
"""
if isinstance(dt, str):
dt_dt = dateutil.parser.parse(dt) # type: ignore
return cls.parse_datetime(dt_dt)

if isinstance(dt, dict):
for key in "dateTime", "date":
if key in dt:
date_time = cast(str, dt.get(key))
break
else:
raise RuntimeError(f"Invalid structure dict: {dt}")

return cls.parse_datetime(date_time)

if isinstance(dt, datetime.datetime):
return assume_local_tz_if_none(dt)

raise TypeError(
f"Unexpected type of a given date item, type: {type(dt)}, contents: {dt}",
)

@classmethod
def items_are_identical(cls, item1, item2, ignore_keys: Sequence[str] = []) -> bool:
for item in [item1, item2]:
Expand Down
70 changes: 5 additions & 65 deletions syncall/google/gtasks_side.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
from pathlib import Path
from typing import TYPE_CHECKING, Sequence, cast

import dateutil
import pkg_resources
import pytz
from bubop import format_datetime_tz, logger
from bubop import logger
from googleapiclient import discovery
from googleapiclient.http import HttpError

from syncall.google.google_side import GoogleSide
from syncall.sync_side import SyncSide

from .common import parse_google_datetime

if TYPE_CHECKING:
from syncall.types import GTasksItem, GTasksList

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

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

@staticmethod
def format_datetime(dt: datetime.datetime) -> str:
assert isinstance(dt, datetime.datetime)
return format_datetime_tz(dt)

@classmethod
def parse_datetime(cls, dt: str | dict | datetime.datetime) -> datetime.datetime:
"""Parse datetime given in the GTasks format(s):
- string with ('T', 'Z' separators).
- (dateTime, dateZone) dictionary
- datetime object
Usage::
>>> GTasksSide.parse_datetime("2019-03-05T00:03:09Z")
datetime.datetime(2019, 3, 5, 0, 3, 9)
>>> GTasksSide.parse_datetime("2019-03-05")
datetime.datetime(2019, 3, 5, 0, 0)
>>> GTasksSide.parse_datetime("2019-03-05T00:03:01.1234Z")
datetime.datetime(2019, 3, 5, 0, 3, 1, 123400)
>>> GTasksSide.parse_datetime("2019-03-08T00:29:06.602Z")
datetime.datetime(2019, 3, 8, 0, 29, 6, 602000)
>>> from tzlocal import get_localzone_name
>>> tz = get_localzone_name()
>>> a = GTasksSide.parse_datetime({"dateTime": "2021-11-14T22:07:49Z", "timeZone": tz})
>>> b = GTasksSide.parse_datetime({"dateTime": "2021-11-14T22:07:49.000000Z"})
>>> b
datetime.datetime(2021, 11, 14, 22, 7, 49)
>>> from bubop.time import is_same_datetime
>>> is_same_datetime(a, b) or (print(a) or print(b))
True
>>> GTasksSide.parse_datetime({"dateTime": "2021-11-14T22:07:49.123456"})
datetime.datetime(2021, 11, 14, 22, 7, 49, 123456)
>>> a = GTasksSide.parse_datetime({"dateTime": "2021-11-14T22:07:49Z", "timeZone": tz})
>>> GTasksSide.parse_datetime(a).isoformat() == a.isoformat()
True
"""
if isinstance(dt, str):
return dateutil.parser.parse(dt).replace(tzinfo=None) # type: ignore

if isinstance(dt, dict):
date_time = dt.get("dateTime")
if date_time is None:
raise RuntimeError(f"Invalid structure dict: {dt}")
dt_dt = GTasksSide.parse_datetime(date_time)
time_zone = dt.get("timeZone")
if time_zone is not None:
timezone = pytz.timezone(time_zone)
dt_dt = timezone.localize(dt_dt)

return dt_dt

if isinstance(dt, datetime.datetime):
return dt

raise TypeError(
f"Unexpected type of a given date item, type: {type(dt)}, contents: {dt}",
)

@classmethod
def items_are_identical(cls, item1, item2, ignore_keys: Sequence[str] = []) -> bool:
for item in [item1, item2]:
for key in cls._date_keys:
if key not in item:
continue

item[key] = cls.parse_datetime(item[key])
item[key] = parse_google_datetime(item[key])

return SyncSide._items_are_identical(
item1,
Expand Down
12 changes: 5 additions & 7 deletions syncall/tw_gcal_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import timedelta
from uuid import UUID

from bubop import logger
from bubop import format_datetime_tz, logger
from item_synchronizer.types import Item

from syncall.google.gcal_side import GCalSide
Expand Down Expand Up @@ -84,11 +84,9 @@ def convert_tw_to_gcal(
f'Using "{date_key}" date for {tw_item["uuid"]} for setting the end date of'
" the event",
)
dt_gcal = GCalSide.format_datetime(tw_item[date_key])
dt_gcal = format_datetime_tz(tw_item[date_key])
gcal_item["start"] = {
"dateTime": GCalSide.format_datetime(
tw_item[date_key] - tw_item[tw_duration_key],
),
"dateTime": format_datetime_tz(tw_item[date_key] - tw_item[tw_duration_key]),
}
gcal_item["end"] = {"dateTime": dt_gcal}
break
Expand All @@ -98,12 +96,12 @@ def convert_tw_to_gcal(
" event",
)
entry_dt = tw_item["entry"]
entry_dt_gcal_str = GCalSide.format_datetime(entry_dt)
entry_dt_gcal_str = format_datetime_tz(entry_dt)

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

gcal_item["end"] = {
"dateTime": GCalSide.format_datetime(entry_dt + tw_item[tw_duration_key]),
"dateTime": format_datetime_tz(entry_dt + tw_item[tw_duration_key]),
}

return gcal_item
Expand Down
7 changes: 3 additions & 4 deletions syncall/tw_gtasks_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from bubop import logger
from bubop import format_datetime_tz, logger
from item_synchronizer.types import Item

from syncall.google.common import parse_google_datetime
from syncall.google.gtasks_side import GTasksSide
from syncall.tw_utils import extract_tw_fields_from_string, get_tw_annotations_as_str
from syncall.types import GTasksItem
Expand Down Expand Up @@ -28,9 +29,7 @@ def convert_tw_to_gtask(

# update time
if "modified" in tw_item.keys():
gtasks_item["updated"] = GTasksSide.format_datetime(
GTasksSide.parse_datetime(tw_item["modified"]),
)
gtasks_item["updated"] = format_datetime_tz(parse_google_datetime(tw_item["modified"]))

return gtasks_item

Expand Down

0 comments on commit 3d41b14

Please sign in to comment.