Skip to content

Commit

Permalink
Add a --pdb-on-error flag to tw_caldav_sync
Browse files Browse the repository at this point in the history
Also includes:

* Bump item_sync and bubop
* Rename twgcalsyncduration to syncallduration and use it across all
  sides
* Fix bug with syncallduration when adding/updating items to taskwarrior
  • Loading branch information
bergercookie committed Dec 31, 2023
1 parent f655172 commit b57c6b4
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 68 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,5 @@ test-tw-gcal-sync.json
/a/
/.taskrc
/.task
/.task_backup
/.envrc
30 changes: 11 additions & 19 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ loguru = "^0.5.3"
python-dateutil = "^2.8.1"
rfc3339 = "^6.2"
typing = "^3.7.4"
item-synchronizer = "^1.1.4"
bubop = { version = "0.1.11", allow-prereleases = true }
item-synchronizer = "^1.1.5"
bubop = { version = "0.1.12", allow-prereleases = true }
# item-synchronizer = {path="/home/berger/src/item_synchronizer", develop=true}
# bubop = {path="/home/berger/src/bubop", develop=true}

[tool.poetry.extras]
google = [
Expand Down Expand Up @@ -111,7 +113,7 @@ coverage = { version = "^6.2", extras = ["toml"] }
coveralls = "^3.3.1"
pycln = "^1.3.1"
check-jsonschema = "^0.14.3"
readline = "6.2.4.1"
# readline = "6.2.4.1"

# isort ------------------------------------------------------------------------

Expand Down
2 changes: 2 additions & 0 deletions syncall/aggregator.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def __init__(
resolution_strategy: ResolutionStrategy = AlwaysSecondRS(),
config_fname: Optional[str] = None,
ignore_keys: Tuple[Sequence[str], Sequence[str]] = tuple(),
catch_exceptions: bool = False
):
# Preferences manager
# Sample config path: ~/.config/syncall/taskwarrior_gcal_sync.yaml
Expand Down Expand Up @@ -112,6 +113,7 @@ def side_A_fn(fn):
item_getter_A=side_A_fn(self.item_getter_for),
item_getter_B=side_B_fn(self.item_getter_for),
resolution_strategy=self._resolution_strategy,
catch_exceptions=catch_exceptions,
side_names=(side_A.fullname, side_B.fullname),
)

Expand Down
41 changes: 40 additions & 1 deletion syncall/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,52 @@
This module will be loaded regardless of extras - don't put something here that requires an
extra dependency.
"""

import sys

import click
from bubop.common_dir import sys

from syncall.app_utils import name_to_resolution_strategy_type
from syncall.constants import COMBINATION_FLAGS


def _set_own_excepthook(ctx, param, value):
if not value or ctx.resilient_parsing:
return value

sys.excepthook = _run_pdb_on_error
return value


def _run_pdb_on_error(type, value, tb):
if hasattr(sys, "ps1") or not sys.stderr.isatty():
# we are in interactive mode or we don't have a tty-like device, so we call the
# default hook
print(f"Cannot enable the --pdb-on-error flag")
sys.__excepthook__(type, value, tb)
else:
import pdb
import traceback

traceback.print_exception(type, value, tb)
if type is KeyboardInterrupt:
return

pdb.pm()


def opt_pdb_on_error():
return click.option(
"--pdb-on-error",
"pdb_on_error",
is_flag=True,
help="Invoke PDB if there's an uncaught exception during the program execution",
callback=_set_own_excepthook,
expose_value=True,
is_eager=True,
)


def opt_asana_task_gid(**kwargs):
return click.option(
"-a",
Expand Down
83 changes: 50 additions & 33 deletions syncall/scripts/tw_caldav_sync.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import atexit
import datetime
import os
import subprocess
Expand All @@ -11,6 +12,7 @@
format_dict,
logger,
loguru_tqdm_sink,
ExitHooks,
)

from syncall import inform_about_app_extras
Expand All @@ -21,6 +23,7 @@
opt_caldav_passwd_pass_path,
opt_caldav_url,
opt_caldav_user,
opt_pdb_on_error,
opt_tw_all_tasks,
opt_tw_only_tasks_modified_30_days,
)
Expand Down Expand Up @@ -70,6 +73,7 @@
@opt_custom_combination_savename("TW", "Caldav")
@click.option("-v", "--verbose", count=True)
@click.version_option(__version__)
@opt_pdb_on_error()
def main(
caldav_calendar: str,
caldav_url: str,
Expand All @@ -85,6 +89,7 @@ def main(
custom_combination_savename: str,
resolution_strategy: str,
do_list_combinations: bool,
pdb_on_error: bool,
):
"""Synchronize lists of tasks from your caldav Calendar with filters from Taskwarrior.
Expand Down Expand Up @@ -113,14 +118,12 @@ def main(
)

check_optional_mutually_exclusive(combination_name, custom_combination_savename)
combination_of_tw_project_tags_and_caldav_calendar = any(
[
tw_project,
tw_tags,
tw_sync_all_tasks,
caldav_calendar,
]
)
combination_of_tw_project_tags_and_caldav_calendar = any([
tw_project,
tw_tags,
tw_sync_all_tasks,
caldav_calendar,
])
check_optional_mutually_exclusive(
combination_name, combination_of_tw_project_tags_and_caldav_calendar
)
Expand Down Expand Up @@ -207,32 +210,46 @@ def main(
client = caldav.DAVClient(url=caldav_url, username=caldav_user, password=caldav_passwd)
caldav_side = CaldavSide(client=client, calendar_name=caldav_calendar)

# teardown function and exception handling ------------------------------------------------
hooks: ExitHooks = ExitHooks()

def teardown():
if hooks.exception is not None:
if hooks.exception.__class__ is KeyboardInterrupt:
logger.error("C-c pressed, exiting...")
else:
report_toplevel_exception(is_verbose=verbose >= 1)
return 1

if inform_about_config:
inform_about_combination_name_usage(combination_name)

if pdb_on_error:
logger.warning(
"pdb_on_error is enabled. Disabling exit hooks / not taking actions at the end "
"of the run."
)
else:
hooks.register()
atexit.register(teardown)

# sync ------------------------------------------------------------------------------------
try:
with Aggregator(
side_A=caldav_side,
side_B=tw_side,
converter_B_to_A=convert_tw_to_caldav,
converter_A_to_B=convert_caldav_to_tw,
resolution_strategy=get_resolution_strategy(
resolution_strategy, side_A_type=type(caldav_side), side_B_type=type(tw_side)
),
config_fname=combination_name,
ignore_keys=(
(),
(),
),
) as aggregator:
aggregator.sync()
except KeyboardInterrupt:
logger.error("Exiting...")
return 1
except:
report_toplevel_exception(is_verbose=verbose >= 1)
return 1

if inform_about_config:
inform_about_combination_name_usage(combination_name)
with Aggregator(
side_A=caldav_side,
side_B=tw_side,
converter_B_to_A=convert_tw_to_caldav,
converter_A_to_B=convert_caldav_to_tw,
resolution_strategy=get_resolution_strategy(
resolution_strategy, side_A_type=type(caldav_side), side_B_type=type(tw_side)
),
config_fname=combination_name,
ignore_keys=(
(),
(),
),
catch_exceptions=not pdb_on_error,
) as aggregator:
aggregator.sync()

return 0

Expand Down
23 changes: 19 additions & 4 deletions syncall/taskwarrior/taskw_duration.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from syncall.types import TaskwarriorRawItem

tw_duration_key = "twgcalsyncduration"
tw_duration_key = "syncallduration"


def extract_part(s: str, split: str) -> Tuple[float, str]:
Expand Down Expand Up @@ -108,7 +108,7 @@ def parse_iso8601_duration(string: str) -> timedelta:
return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)


def duration_serialize(value: timedelta) -> str:
def taskw_duration_serialize(value: timedelta) -> str:
"""
>>> duration_serialize(timedelta(days=300))
'PT25920000S'
Expand All @@ -121,7 +121,7 @@ def duration_serialize(value: timedelta) -> str:
return "PT{}S".format(int(value.total_seconds()))


def duration_deserialize(value: str) -> timedelta:
def taskw_duration_deserialize(value: str) -> timedelta:
return parse_iso8601_duration(value)


Expand All @@ -131,9 +131,24 @@ def convert_tw_duration_to_timedelta(
if tw_duration_key in item.keys():
duration = item[tw_duration_key]
if isinstance(duration, str):
duration: datetime.timedelta = duration_deserialize(duration)
duration: datetime.timedelta = taskw_duration_deserialize(duration)
assert isinstance(duration, datetime.timedelta)
else:
duration = default_duration

item[tw_duration_key] = duration

def convert_tw_duration_serialize(
item: TaskwarriorRawItem, default_duration=datetime.timedelta(hours=1)
) -> None:
if tw_duration_key in item.keys():
duration = item[tw_duration_key]
if isinstance(duration, str):
duration: datetime.timedelta = taskw_duration_deserialize(duration)
assert isinstance(duration, datetime.timedelta)
else:
duration = default_duration


item[tw_duration_key] = taskw_duration_serialize(value=duration)

3 changes: 2 additions & 1 deletion syncall/taskwarrior/taskwarrior_side.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from syncall.sync_side import ItemType, SyncSide
from syncall.taskwarrior.taskw_duration import (
convert_tw_duration_to_timedelta,
convert_tw_duration_serialize,
tw_duration_key,
)
from syncall.types import TaskwarriorRawItem
Expand Down Expand Up @@ -238,7 +239,7 @@ def add_item(self, item: ItemType) -> ItemType:
description = item.pop("description")
len_print = min(20, len(description))

convert_tw_duration_to_timedelta(item=item)
convert_tw_duration_serialize(item=item)

logger.trace(f'Adding task "{description[0:len_print]}" with properties:\n\n{item}')
new_item = self._tw.task_add(description=description, **item) # type: ignore
Expand Down
14 changes: 7 additions & 7 deletions syncall/tw_gcal_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from syncall.google.gcal_side import GCalSide
from syncall.taskwarrior.taskw_duration import convert_tw_duration_to_timedelta
from syncall.taskwarrior.taskw_duration import duration_serialize as taskw_duration_serialize
from syncall.taskwarrior.taskw_duration import taskw_duration_serialize
from syncall.taskwarrior.taskw_duration import tw_duration_key
from syncall.tw_utils import (
extract_tw_fields_from_string,
Expand Down Expand Up @@ -40,9 +40,9 @@ def _add_success_prefix(gcal_item: Item):


def _add_failed_prefix(gcal_item: Item):
gcal_item[
"summary"
] = f'{_prefix_title_failed_str}{gcal_item["summary"][len(_failed_str)+1:]}'
gcal_item["summary"] = (
f'{_prefix_title_failed_str}{gcal_item["summary"][len(_failed_str)+1:]}'
)


def convert_tw_to_gcal(
Expand All @@ -64,9 +64,9 @@ def convert_tw_to_gcal(

# description
gcal_item["description"] = "IMPORTED FROM TASKWARRIOR\n"
gcal_item["description"] += "\n".join(
[get_tw_annotations_as_str(tw_item), get_tw_status_and_uuid_as_str(tw_item)]
)
gcal_item["description"] += "\n".join([
get_tw_annotations_as_str(tw_item), get_tw_status_and_uuid_as_str(tw_item)
])

date_keys = ["scheduled", "due"] if prefer_scheduled_date else ["due", "scheduled"]
# event duration --------------------------------------------------------------------------
Expand Down

0 comments on commit b57c6b4

Please sign in to comment.