Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
bergercookie committed Jan 18, 2024
1 parent 5565983 commit d872557
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 96 deletions.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ gkeepapi = { version = "^0.13.7", optional = true }
asana = { version = "^1.0.0", optional = true }
caldav = { version = "^0.11.0", optional = true }
icalendar = { version = "^5.0.3", optional = true }
taskw = { version = "^1.3.1", optional = true }
# taskw = { version = "^1.3.1", optional = true }
taskw = { path = "/home/berger/src/taskw", develop = true }
xattr = { version = "^0.9.9", optional = true }
xdg = { version = "^6.0.0", optional = true }

Expand Down
26 changes: 18 additions & 8 deletions syncall/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ def decorator(f):
opt_tw_all_tasks,
opt_tw_tags,
opt_tw_project,
opt_tw_only_tasks_modified_30_days,
opt_tw_only_tasks_modified_X_days,
opt_prefer_scheduled_date,
]):
f = d()(f)
return f
Expand Down Expand Up @@ -157,14 +158,23 @@ def callback(ctx, param, value):
)


# TODO
def opt_tw_only_tasks_modified_30_days():
def opt_tw_only_tasks_modified_X_days():
def callback(ctx, param, value):
if value is None or ctx.resilient_parsing:
return

return f"modified.after:-{value}d"

return click.option(
"--30-days",
"--days",
"--only-modified-last-X-days",
"tw_only_modified_last_30_days",
is_flag=True,
help="Only synchronize Taskwarrior tasks that have been modified in the last 30 days",
"tw_only_modified_last_X_days",
type=str,
help=(
"Only synchronize Taskwarrior tasks that have been modified in the last X days"
" (specify X, e.g., 1, 30, 0.5, etc.)"
),
callback=callback,
)


Expand All @@ -180,7 +190,7 @@ def opt_prefer_scheduled_date():
)


# notion
# notion --------------------------------------------------------------------------------------
def opt_notion_page_id():
return click.option(
"-n",
Expand Down
10 changes: 5 additions & 5 deletions syncall/scripts/tw_caldav_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
opt_caldav_user,
opt_pdb_on_error,
opt_tw_all_tasks,
opt_tw_only_tasks_modified_30_days,
opt_tw_only_tasks_modified_X_days,
)
from syncall.tw_caldav_utils import convert_caldav_to_tw, convert_tw_to_caldav

Expand Down Expand Up @@ -65,7 +65,7 @@
@opt_tw_all_tasks()
@opt_tw_tags()
@opt_tw_project()
@opt_tw_only_tasks_modified_30_days()
@opt_tw_only_tasks_modified_X_days()
# misc options --------------------------------------------------------------------------------
@opt_list_combinations("TW", "Caldav")
@opt_resolution_strategy()
Expand All @@ -83,7 +83,7 @@ def main(
tw_sync_all_tasks: bool,
tw_tags: List[str],
tw_project: str,
tw_only_modified_last_30_days: bool,
tw_only_modified_last_X_days: bool,
verbose: int,
combination_name: str,
custom_combination_savename: str,
Expand Down Expand Up @@ -148,7 +148,7 @@ def main(
"tw_project": tw_project,
"tw_tags": tw_tags,
"tw_sync_all_tasks": tw_sync_all_tasks,
"tw_only_modified_last_30_days": tw_only_modified_last_30_days,
"tw_only_modified_last_X_days": tw_only_modified_last_X_days,
},
config_fname="tw_caldav_configs",
custom_combination_savename=custom_combination_savename,
Expand All @@ -173,7 +173,7 @@ def main(
# TW
# TODO abstract this
only_modified_since = None
if tw_only_modified_last_30_days:
if tw_only_modified_last_X_days:
only_modified_since = datetime.datetime.now() - datetime.timedelta(days=30)

tw_side = TaskWarriorSide(
Expand Down
65 changes: 28 additions & 37 deletions syncall/scripts/tw_gtasks_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import click
from bubop import (
check_optional_mutually_exclusive,
check_required_mutually_exclusive,
format_dict,
log_to_syslog,
logger,
Expand Down Expand Up @@ -37,11 +38,8 @@
opt_gtasks_list,
opt_list_combinations,
opt_list_resolution_strategies,
opt_prefer_scheduled_date,
opt_pdb_on_error,
opt_resolution_strategy,
opt_tw_filter,
opt_tw_project,
opt_tw_tags,
opts_tw_filtering,
)

Expand All @@ -59,33 +57,32 @@
@opt_resolution_strategy()
@opt_combination("TW", "Google Tasks")
@opt_custom_combination_savename("TW", "Google Tasks")
@opt_prefer_scheduled_date()
@click.option("-v", "--verbose", count=True)
@click.version_option(__version__)
@opt_pdb_on_error()
def main(
gtasks_list: str,
google_secret: str,
oauth_port: int,

# tw
tw_filter: str,
tw_tags: List[str],
tw_tags: str,
tw_project: str,
tw_only_modified_last_X_days: str,
tw_sync_all_tasks: bool,
tw_only_modified_last_30_days: bool,

prefer_scheduled_date: bool,
resolution_strategy: str,
verbose: int,
combination_name: str,
custom_combination_savename: str,
do_list_combinations: bool,
list_resolution_strategies: bool, # type: ignore
prefer_scheduled_date: bool,
pdb_on_error: bool,
):
"""Synchronize lists from your Google Tasks with filters from Taskwarrior.
The list of TW tasks is determined by a combination of TW tags and a TW project while the
list in GTasks should be provided by their name. if it doesn't exist it will be crated
The list of TW tasks can be based on a TW project, tag, on the modification date or on an
arbitrary filter while the list in GTasks should be provided by their name. if it doesn't
exist it will be created.
"""
# setup logger ----------------------------------------------------------------------------
loguru_tqdm_sink(verbosity=verbose)
Expand All @@ -98,25 +95,25 @@ def main(
return 0

# cli validation --------------------------------------------------------------------------
tw_filter_li = [t for t in [
tw_filter,
tw_tags,
tw_project,
tw_only_modified_last_X_days,
] if t]

check_optional_mutually_exclusive(combination_name, custom_combination_savename)
combination_of_tw_project_tags_and_gtasks_list = any(
[
tw_project,
tw_tags,
gtasks_list,
]
)
combination_of_tw_filter_and_gtasks_list = any([ tw_filter_li, tw_sync_all_tasks, gtasks_list, ])
check_optional_mutually_exclusive(
combination_name, combination_of_tw_project_tags_and_gtasks_list
combination_name, combination_of_tw_filter_and_gtasks_list
)

# existing combination name is provided ---------------------------------------------------
if combination_name is not None:
app_config = fetch_app_configuration(
config_fname="tw_gtasks_configs", combination=combination_name
)
tw_tags = app_config["tw_tags"]
tw_project = app_config["tw_project"]
tw_filter_li = app_config["tw_filter_li"]
gtasks_list = app_config["gtasks_list"]

# combination manually specified ----------------------------------------------------------
Expand All @@ -125,23 +122,17 @@ def main(
combination_name = cache_or_reuse_cached_combination(
config_args={
"gtasks_list": gtasks_list,
"tw_project": tw_project,
"tw_tags": tw_tags,
"tw_filter_li": tw_filter_li,
},
config_fname="tw_gtasks_configs",
custom_combination_savename=custom_combination_savename,
)

# at least one of tw_tags, tw_project should be set ---------------------------------------
if not tw_tags and not tw_project:
logger.error(
"You have to provide at least one valid tag or a valid project ID to use for the"
" synchronization. You can do so either via CLI arguments or by specifying an"
" existing saved combination"
)
sys.exit(1)

# more checks -----------------------------------------------------------------------------
check_required_mutually_exclusive(
tw_sync_all_tasks, tw_filter_li, "sync_all_tw_tasks", "tw_filter_li"
)

if gtasks_list is None:
logger.error(
"You have to provide the name of a Google Tasks list to synchronize events"
Expand All @@ -155,8 +146,8 @@ def main(
format_dict(
header="Configuration",
items={
"TW Tags": tw_tags,
"TW Project": tw_project,
"TW Filter": " ".join(tw_filter_li),
"TW Sync All Tasks": tw_sync_all_tasks,
"Google Tasks": gtasks_list,
"Prefer scheduled dates": prefer_scheduled_date,
},
Expand All @@ -166,7 +157,7 @@ def main(
)

# initialize sides ------------------------------------------------------------------------
tw_side = TaskWarriorSide(tags=tw_tags, project=tw_project)
tw_side = TaskWarriorSide(tw_filter=tw_filter_li)

gtask_side = GTasksSide(
task_list_title=gtasks_list, oauth_port=oauth_port, client_secret=google_secret
Expand Down
52 changes: 7 additions & 45 deletions syncall/taskwarrior/taskwarrior_side.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def __init__(
self,
tags: Sequence[str] = tuple(),
project: Optional[str] = None,
only_modified_since: Optional[datetime.datetime] = None,
tw_filter: str,
config_file_override: Optional[Path] = None,
config_overrides: Mapping[str, Any] = {},
**kargs,
Expand All @@ -69,14 +69,16 @@ def __init__(
:param tags: Only include tasks that have are tagged using *all* the specified tags
:param project: Only include tasks that include in this project
:param only_modified_since: Only include tasks that are modified since the specified date
:param tw_filter: Arbitrary taskwarrior filter to use for determining the list of tasks
to sync
:param config_file: Path to the taskwarrior RC file
:param config_overrides: Dictionary of taskrc key, values to override. See also
tw_config_default_overrides
"""
super().__init__(name="Tw", fullname="Taskwarrior", **kargs)
self._tags: Set[str] = set(tags)
self._project: str = project or ""
self._tw_filter: str = tw_filter

config_overrides_ = tw_config_default_overrides.copy()
config_overrides_.update(config_overrides)
Expand Down Expand Up @@ -114,8 +116,6 @@ def __init__(
# Whether to refresh the cached list of items
self._reload_items = True

self._only_modified_since = only_modified_since

def start(self):
logger.info(f"Initializing {self.fullname}...")

Expand All @@ -128,7 +128,9 @@ def _load_all_items(self):
if not self._reload_items:
return

tasks = self._tw.load_tasks()
filter_ = [*[f"+{tag}" for tag in self._tags], f"pro:{self._project}", self._tw_filter]
tasks = self._tw.load_tasks_and_filter(command="all", filter_=filter_)

items = [*tasks["completed"], *tasks["pending"]]
self._items_cache: Dict[str, TaskwarriorRawItem] = { # type: ignore
str(item["uuid"]): item for item in items
Expand Down Expand Up @@ -157,46 +159,6 @@ def get_all_items(
if skip_completed:
tasks = [t for t in tasks if t["status"] != "completed"] # type: ignore

# filter the tasks based on their tags, project and modification date -----------------
def create_tasks_filter() -> Callable[[TaskwarriorRawItem], bool]:
always_true = lambda task: True
fn = always_true

tags_fn = lambda task: self._tags.issubset(task.get("tags", []))
project_fn = lambda task: task.get("project", "").startswith(self._project)

if self._only_modified_since:
mod_since_date = assume_local_tz_if_none(self._only_modified_since)

def only_modified_since_fn(task):
mod_date = task.get("modified")
if mod_date is None:
logger.warning(
f'Task does not have a modification date {task["uuid"]}, this'
" sounds like a bug but including it anyway..."
)
return True

mod_date: datetime.datetime
mod_date = assume_local_tz_if_none(mod_date)

if mod_since_date <= mod_date:
return True

return False

if self._tags:
fn = lambda task, fn=fn, tags_fn=tags_fn: fn(task) and tags_fn(task)
if self._project:
fn = lambda task, fn=fn, project_fn=project_fn: fn(task) and project_fn(task)
if self._only_modified_since:
fn = lambda task, fn=fn: fn(task) and only_modified_since_fn(task)

return fn

tasks_filter = create_tasks_filter()
tasks = [t for t in tasks if tasks_filter(t)]

for task in tasks:
task["uuid"] = str(task["uuid"]) # type: ignore

Expand Down

0 comments on commit d872557

Please sign in to comment.