Skip to content

Commit 8007c19

Browse files
committed
WIP
1 parent 04b39c5 commit 8007c19

File tree

5 files changed

+79
-93
lines changed

5 files changed

+79
-93
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ gkeepapi = { version = "^0.13.7", optional = true }
6464
asana = { version = "^1.0.0", optional = true }
6565
caldav = { version = "^0.11.0", optional = true }
6666
icalendar = { version = "^5.0.3", optional = true }
67-
taskw = { version = "^1.3.1", optional = true }
67+
# taskw = { version = "^1.3.1", optional = true }
68+
taskw = { path = "/home/berger/src/taskw", develop = true }
6869
xattr = { version = "^0.9.9", optional = true }
6970
xdg = { version = "^6.0.0", optional = true }
7071

syncall/cli.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ def decorator(f):
9191
opt_tw_all_tasks,
9292
opt_tw_tags,
9393
opt_tw_project,
94-
opt_tw_only_tasks_modified_30_days,
94+
opt_tw_only_tasks_modified_X_days,
95+
opt_prefer_scheduled_date,
9596
]):
9697
f = d()(f)
9798
return f
@@ -124,7 +125,7 @@ def callback(ctx, param, value):
124125
if value is None or ctx.resilient_parsing:
125126
return
126127

127-
return " ".join(f"+{one_val}" for one_val in value)
128+
return value
128129

129130
return click.option(
130131
"-t",
@@ -157,14 +158,23 @@ def callback(ctx, param, value):
157158
)
158159

159160

160-
# TODO
161-
def opt_tw_only_tasks_modified_30_days():
161+
def opt_tw_only_tasks_modified_X_days():
162+
def callback(ctx, param, value):
163+
if value is None or ctx.resilient_parsing:
164+
return
165+
166+
return f"modified.after:-{value}d"
167+
162168
return click.option(
163-
"--30-days",
169+
"--days",
164170
"--only-modified-last-X-days",
165-
"tw_only_modified_last_30_days",
166-
is_flag=True,
167-
help="Only synchronize Taskwarrior tasks that have been modified in the last 30 days",
171+
"tw_only_modified_last_X_days",
172+
type=str,
173+
help=(
174+
"Only synchronize Taskwarrior tasks that have been modified in the last X days"
175+
" (specify X, e.g., 1, 30, 0.5, etc.)"
176+
),
177+
callback=callback,
168178
)
169179

170180

@@ -180,7 +190,7 @@ def opt_prefer_scheduled_date():
180190
)
181191

182192

183-
# notion
193+
# notion --------------------------------------------------------------------------------------
184194
def opt_notion_page_id():
185195
return click.option(
186196
"-n",

syncall/scripts/tw_caldav_sync.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
opt_caldav_user,
2626
opt_pdb_on_error,
2727
opt_tw_all_tasks,
28-
opt_tw_only_tasks_modified_30_days,
28+
opt_tw_only_tasks_modified_X_days,
2929
)
3030
from syncall.tw_caldav_utils import convert_caldav_to_tw, convert_tw_to_caldav
3131

@@ -65,7 +65,7 @@
6565
@opt_tw_all_tasks()
6666
@opt_tw_tags()
6767
@opt_tw_project()
68-
@opt_tw_only_tasks_modified_30_days()
68+
@opt_tw_only_tasks_modified_X_days()
6969
# misc options --------------------------------------------------------------------------------
7070
@opt_list_combinations("TW", "Caldav")
7171
@opt_resolution_strategy()
@@ -83,7 +83,7 @@ def main(
8383
tw_sync_all_tasks: bool,
8484
tw_tags: List[str],
8585
tw_project: str,
86-
tw_only_modified_last_30_days: bool,
86+
tw_only_modified_last_X_days: bool,
8787
verbose: int,
8888
combination_name: str,
8989
custom_combination_savename: str,
@@ -148,7 +148,7 @@ def main(
148148
"tw_project": tw_project,
149149
"tw_tags": tw_tags,
150150
"tw_sync_all_tasks": tw_sync_all_tasks,
151-
"tw_only_modified_last_30_days": tw_only_modified_last_30_days,
151+
"tw_only_modified_last_X_days": tw_only_modified_last_X_days,
152152
},
153153
config_fname="tw_caldav_configs",
154154
custom_combination_savename=custom_combination_savename,
@@ -173,7 +173,7 @@ def main(
173173
# TW
174174
# TODO abstract this
175175
only_modified_since = None
176-
if tw_only_modified_last_30_days:
176+
if tw_only_modified_last_X_days:
177177
only_modified_since = datetime.datetime.now() - datetime.timedelta(days=30)
178178

179179
tw_side = TaskWarriorSide(

syncall/scripts/tw_gtasks_sync.py

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import click
55
from bubop import (
66
check_optional_mutually_exclusive,
7+
check_required_mutually_exclusive,
78
format_dict,
89
log_to_syslog,
910
logger,
@@ -37,11 +38,8 @@
3738
opt_gtasks_list,
3839
opt_list_combinations,
3940
opt_list_resolution_strategies,
40-
opt_prefer_scheduled_date,
41+
opt_pdb_on_error,
4142
opt_resolution_strategy,
42-
opt_tw_filter,
43-
opt_tw_project,
44-
opt_tw_tags,
4543
opts_tw_filtering,
4644
)
4745

@@ -59,33 +57,32 @@
5957
@opt_resolution_strategy()
6058
@opt_combination("TW", "Google Tasks")
6159
@opt_custom_combination_savename("TW", "Google Tasks")
62-
@opt_prefer_scheduled_date()
6360
@click.option("-v", "--verbose", count=True)
6461
@click.version_option(__version__)
62+
@opt_pdb_on_error()
6563
def main(
6664
gtasks_list: str,
6765
google_secret: str,
6866
oauth_port: int,
69-
70-
# tw
7167
tw_filter: str,
7268
tw_tags: List[str],
7369
tw_project: str,
70+
tw_only_modified_last_X_days: str,
7471
tw_sync_all_tasks: bool,
75-
tw_only_modified_last_30_days: bool,
76-
72+
prefer_scheduled_date: bool,
7773
resolution_strategy: str,
7874
verbose: int,
7975
combination_name: str,
8076
custom_combination_savename: str,
8177
do_list_combinations: bool,
8278
list_resolution_strategies: bool, # type: ignore
83-
prefer_scheduled_date: bool,
79+
pdb_on_error: bool,
8480
):
8581
"""Synchronize lists from your Google Tasks with filters from Taskwarrior.
8682
87-
The list of TW tasks is determined by a combination of TW tags and a TW project while the
88-
list in GTasks should be provided by their name. if it doesn't exist it will be crated
83+
The list of TW tasks can be based on a TW project, tag, on the modification date or on an
84+
arbitrary filter while the list in GTasks should be provided by their name. if it doesn't
85+
exist it will be created.
8986
"""
9087
# setup logger ----------------------------------------------------------------------------
9188
loguru_tqdm_sink(verbosity=verbose)
@@ -98,23 +95,33 @@ def main(
9895
return 0
9996

10097
# cli validation --------------------------------------------------------------------------
101-
check_optional_mutually_exclusive(combination_name, custom_combination_savename)
102-
combination_of_tw_project_tags_and_gtasks_list = any(
103-
[
104-
tw_project,
105-
tw_tags,
106-
gtasks_list,
98+
tw_filter_li = [
99+
t
100+
for t in [
101+
tw_filter,
102+
tw_only_modified_last_X_days,
107103
]
108-
)
104+
if t
105+
]
106+
107+
check_optional_mutually_exclusive(combination_name, custom_combination_savename)
108+
combination_of_tw_filter_and_gtasks_list = any([
109+
tw_filter_li,
110+
tw_tags,
111+
tw_project,
112+
tw_sync_all_tasks,
113+
gtasks_list,
114+
])
109115
check_optional_mutually_exclusive(
110-
combination_name, combination_of_tw_project_tags_and_gtasks_list
116+
combination_name, combination_of_tw_filter_and_gtasks_list
111117
)
112118

113119
# existing combination name is provided ---------------------------------------------------
114120
if combination_name is not None:
115121
app_config = fetch_app_configuration(
116122
config_fname="tw_gtasks_configs", combination=combination_name
117123
)
124+
tw_filter_li = app_config["tw_filter_li"]
118125
tw_tags = app_config["tw_tags"]
119126
tw_project = app_config["tw_project"]
120127
gtasks_list = app_config["gtasks_list"]
@@ -125,23 +132,23 @@ def main(
125132
combination_name = cache_or_reuse_cached_combination(
126133
config_args={
127134
"gtasks_list": gtasks_list,
128-
"tw_project": tw_project,
135+
"tw_filter_li": tw_filter_li,
129136
"tw_tags": tw_tags,
137+
"tw_project": tw_project,
130138
},
131139
config_fname="tw_gtasks_configs",
132140
custom_combination_savename=custom_combination_savename,
133141
)
134142

135-
# at least one of tw_tags, tw_project should be set ---------------------------------------
136-
if not tw_tags and not tw_project:
137-
logger.error(
138-
"You have to provide at least one valid tag or a valid project ID to use for the"
139-
" synchronization. You can do so either via CLI arguments or by specifying an"
140-
" existing saved combination"
141-
)
142-
sys.exit(1)
143-
144143
# more checks -----------------------------------------------------------------------------
144+
combination_of_tw_related_options = any([tw_filter_li, tw_tags, tw_project])
145+
check_required_mutually_exclusive(
146+
tw_sync_all_tasks,
147+
combination_of_tw_related_options,
148+
"sync_all_tw_tasks",
149+
"combination of specific TW-related options",
150+
)
151+
145152
if gtasks_list is None:
146153
logger.error(
147154
"You have to provide the name of a Google Tasks list to synchronize events"
@@ -155,8 +162,10 @@ def main(
155162
format_dict(
156163
header="Configuration",
157164
items={
165+
"TW Filter": " ".join(tw_filter_li),
158166
"TW Tags": tw_tags,
159167
"TW Project": tw_project,
168+
"TW Sync All Tasks": tw_sync_all_tasks,
160169
"Google Tasks": gtasks_list,
161170
"Prefer scheduled dates": prefer_scheduled_date,
162171
},
@@ -166,7 +175,9 @@ def main(
166175
)
167176

168177
# initialize sides ------------------------------------------------------------------------
169-
tw_side = TaskWarriorSide(tags=tw_tags, project=tw_project)
178+
tw_side = TaskWarriorSide(
179+
tw_filter=" ".join(tw_filter_li), tags=tw_tags, project=tw_project
180+
)
170181

171182
gtask_side = GTasksSide(
172183
task_list_title=gtasks_list, oauth_port=oauth_port, client_secret=google_secret

syncall/taskwarrior/taskwarrior_side.py

Lines changed: 11 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -59,24 +59,28 @@ def __init__(
5959
self,
6060
tags: Sequence[str] = tuple(),
6161
project: Optional[str] = None,
62-
only_modified_since: Optional[datetime.datetime] = None,
62+
tw_filter: str = "",
6363
config_file_override: Optional[Path] = None,
6464
config_overrides: Mapping[str, Any] = {},
6565
**kargs,
6666
):
6767
"""
6868
Constructor.
6969
70-
:param tags: Only include tasks that have are tagged using *all* the specified tags
71-
:param project: Only include tasks that include in this project
72-
:param only_modified_since: Only include tasks that are modified since the specified date
70+
:param tags: Only include tasks that have are tagged using *all* the specified tags.
71+
Also assign these tags to newly added items
72+
:param project: Only include tasks that include in this project. Also assign newly
73+
added items to this project.
74+
:param tw_filter: Arbitrary taskwarrior filter to use for determining the list of tasks
75+
to sync
7376
:param config_file: Path to the taskwarrior RC file
7477
:param config_overrides: Dictionary of taskrc key, values to override. See also
7578
tw_config_default_overrides
7679
"""
7780
super().__init__(name="Tw", fullname="Taskwarrior", **kargs)
7881
self._tags: Set[str] = set(tags)
7982
self._project: str = project or ""
83+
self._tw_filter: str = tw_filter
8084

8185
config_overrides_ = tw_config_default_overrides.copy()
8286
config_overrides_.update(config_overrides)
@@ -114,8 +118,6 @@ def __init__(
114118
# Whether to refresh the cached list of items
115119
self._reload_items = True
116120

117-
self._only_modified_since = only_modified_since
118-
119121
def start(self):
120122
logger.info(f"Initializing {self.fullname}...")
121123

@@ -128,7 +130,9 @@ def _load_all_items(self):
128130
if not self._reload_items:
129131
return
130132

131-
tasks = self._tw.load_tasks()
133+
filter_ = [*[f"+{tag}" for tag in self._tags], f"pro:{self._project}", self._tw_filter]
134+
tasks = self._tw.load_tasks_and_filter(command="all", filter_=filter_)
135+
132136
items = [*tasks["completed"], *tasks["pending"]]
133137
self._items_cache: Dict[str, TaskwarriorRawItem] = { # type: ignore
134138
str(item["uuid"]): item for item in items
@@ -157,46 +161,6 @@ def get_all_items(
157161
if skip_completed:
158162
tasks = [t for t in tasks if t["status"] != "completed"] # type: ignore
159163

160-
# filter the tasks based on their tags, project and modification date -----------------
161-
def create_tasks_filter() -> Callable[[TaskwarriorRawItem], bool]:
162-
always_true = lambda task: True
163-
fn = always_true
164-
165-
tags_fn = lambda task: self._tags.issubset(task.get("tags", []))
166-
project_fn = lambda task: task.get("project", "").startswith(self._project)
167-
168-
if self._only_modified_since:
169-
mod_since_date = assume_local_tz_if_none(self._only_modified_since)
170-
171-
def only_modified_since_fn(task):
172-
mod_date = task.get("modified")
173-
if mod_date is None:
174-
logger.warning(
175-
f'Task does not have a modification date {task["uuid"]}, this'
176-
" sounds like a bug but including it anyway..."
177-
)
178-
return True
179-
180-
mod_date: datetime.datetime
181-
mod_date = assume_local_tz_if_none(mod_date)
182-
183-
if mod_since_date <= mod_date:
184-
return True
185-
186-
return False
187-
188-
if self._tags:
189-
fn = lambda task, fn=fn, tags_fn=tags_fn: fn(task) and tags_fn(task)
190-
if self._project:
191-
fn = lambda task, fn=fn, project_fn=project_fn: fn(task) and project_fn(task)
192-
if self._only_modified_since:
193-
fn = lambda task, fn=fn: fn(task) and only_modified_since_fn(task)
194-
195-
return fn
196-
197-
tasks_filter = create_tasks_filter()
198-
tasks = [t for t in tasks if tasks_filter(t)]
199-
200164
for task in tasks:
201165
task["uuid"] = str(task["uuid"]) # type: ignore
202166

0 commit comments

Comments
 (0)