taskcheck 1.1.1__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {taskcheck-1.1.1/taskcheck.egg-info → taskcheck-1.2.0}/PKG-INFO +4 -1
- {taskcheck-1.1.1 → taskcheck-1.2.0}/pyproject.toml +4 -1
- {taskcheck-1.1.1 → taskcheck-1.2.0}/taskcheck/__main__.py +11 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/taskcheck/common.py +26 -11
- taskcheck-1.2.0/taskcheck/google_calendar.py +172 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/taskcheck/parallel.py +1 -1
- {taskcheck-1.1.1 → taskcheck-1.2.0}/taskcheck/report.py +13 -4
- {taskcheck-1.1.1 → taskcheck-1.2.0/taskcheck.egg-info}/PKG-INFO +4 -1
- {taskcheck-1.1.1 → taskcheck-1.2.0}/taskcheck.egg-info/SOURCES.txt +2 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/taskcheck.egg-info/requires.txt +3 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/tests/test_common.py +44 -1
- taskcheck-1.2.0/tests/test_google_calendar.py +81 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/tests/test_parallel.py +2 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/tests/test_report.py +14 -4
- {taskcheck-1.1.1 → taskcheck-1.2.0}/LICENSE +0 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/README.md +0 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/setup.cfg +0 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/taskcheck/ical.py +0 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/taskcheck/install.py +0 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/taskcheck.egg-info/dependency_links.txt +0 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/taskcheck.egg-info/entry_points.txt +0 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/taskcheck.egg-info/top_level.txt +0 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/tests/test_ical.py +0 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/tests/test_install.py +0 -0
- {taskcheck-1.1.1 → taskcheck-1.2.0}/tests/test_main.py +0 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: taskcheck
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Automatic scheduler for taskwarrior
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
Requires-Dist: appdirs>=1.4.4
|
|
8
|
+
Requires-Dist: google-api-python-client>=2.198.0
|
|
9
|
+
Requires-Dist: google-auth>=2.56.2
|
|
10
|
+
Requires-Dist: google-auth-oauthlib>=1.2.2
|
|
8
11
|
Requires-Dist: icalendar>=6.0.1
|
|
9
12
|
Requires-Dist: random-unicode-emoji>=2.8
|
|
10
13
|
Requires-Dist: requests>=2.32.3
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "taskcheck"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
description = "Automatic scheduler for taskwarrior"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"appdirs>=1.4.4",
|
|
9
|
+
"google-api-python-client>=2.198.0",
|
|
10
|
+
"google-auth>=2.56.2",
|
|
11
|
+
"google-auth-oauthlib>=1.2.2",
|
|
9
12
|
"icalendar>=6.0.1",
|
|
10
13
|
"random-unicode-emoji>=2.8",
|
|
11
14
|
"requests>=2.32.3",
|
|
@@ -54,6 +54,11 @@ arg_parser.add_argument(
|
|
|
54
54
|
default=True,
|
|
55
55
|
help="Disable automatic reduction of urgency weight when tasks cannot be completed on time. (Enabled by default, meaning automatic reduction will occur.)",
|
|
56
56
|
)
|
|
57
|
+
arg_parser.add_argument(
|
|
58
|
+
"--add-google-calendar",
|
|
59
|
+
action="store_true",
|
|
60
|
+
help="Authenticate with Google, list calendars, and print a config section for the selected calendar.",
|
|
61
|
+
)
|
|
57
62
|
|
|
58
63
|
|
|
59
64
|
# Load working hours and exceptions from TOML file
|
|
@@ -100,6 +105,12 @@ def main():
|
|
|
100
105
|
install()
|
|
101
106
|
return
|
|
102
107
|
|
|
108
|
+
if getattr(args, "add_google_calendar", False) is True:
|
|
109
|
+
from taskcheck.google_calendar import add_google_calendar
|
|
110
|
+
|
|
111
|
+
add_google_calendar(taskrc=args.taskrc)
|
|
112
|
+
return
|
|
113
|
+
|
|
103
114
|
if args.schedule:
|
|
104
115
|
config = load_config()
|
|
105
116
|
check_tasks_kwargs = dict(
|
|
@@ -196,7 +196,7 @@ def mark_end_date(
|
|
|
196
196
|
str(id),
|
|
197
197
|
"modify",
|
|
198
198
|
*start_end_fields,
|
|
199
|
-
f
|
|
199
|
+
f"scheduling:{scheduling_note}",
|
|
200
200
|
],
|
|
201
201
|
stdout=subprocess.DEVNULL,
|
|
202
202
|
stderr=subprocess.DEVNULL,
|
|
@@ -211,16 +211,31 @@ def mark_end_date(
|
|
|
211
211
|
def get_calendars(config, verbose=False, force_update=False):
|
|
212
212
|
calendars = []
|
|
213
213
|
for calname in config["calendars"]:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
214
|
+
calendar_config = config["calendars"][calname]
|
|
215
|
+
if (
|
|
216
|
+
calendar_config.get("provider") == "google"
|
|
217
|
+
or calendar_config["url"].startswith("google://")
|
|
218
|
+
):
|
|
219
|
+
from taskcheck.google_calendar import google_calendar_to_dict
|
|
220
|
+
|
|
221
|
+
calendar = google_calendar_to_dict(
|
|
222
|
+
calendar_config.get(
|
|
223
|
+
"calendar_id", calendar_config["url"].removeprefix("google://")
|
|
224
|
+
),
|
|
225
|
+
calendar_config["token_path"],
|
|
226
|
+
config["scheduler"]["days_ahead"],
|
|
227
|
+
all_day=calendar_config["event_all_day_is_blocking"],
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
calendar = ical_to_dict(
|
|
231
|
+
calendar_config["url"],
|
|
232
|
+
config["scheduler"]["days_ahead"],
|
|
233
|
+
all_day=calendar_config["event_all_day_is_blocking"],
|
|
234
|
+
expiration=calendar_config["expiration"],
|
|
235
|
+
verbose=verbose,
|
|
236
|
+
tz_name=calendar_config.get("timezone"),
|
|
237
|
+
force_update=force_update,
|
|
238
|
+
)
|
|
224
239
|
calendar.sort(key=lambda e: e["start"])
|
|
225
240
|
calendars.append(calendar)
|
|
226
241
|
if verbose:
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import warnings
|
|
3
|
+
from datetime import datetime, time, timedelta
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from google.auth.transport.requests import Request
|
|
9
|
+
from google.oauth2.credentials import Credentials
|
|
10
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
11
|
+
from googleapiclient.discovery import build
|
|
12
|
+
|
|
13
|
+
from taskcheck.common import config_dir, get_task_env
|
|
14
|
+
|
|
15
|
+
SCOPES = [
|
|
16
|
+
"openid",
|
|
17
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
18
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
19
|
+
"https://www.googleapis.com/auth/calendar.readonly",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_google_data_dir(taskrc=None):
|
|
24
|
+
env = get_task_env(taskrc)
|
|
25
|
+
base = Path(env.get("TASKDATA", config_dir))
|
|
26
|
+
return base / "google"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_client_secrets_path(taskrc=None):
|
|
30
|
+
repo_secret = Path.cwd() / "client_secret.json"
|
|
31
|
+
if repo_secret.exists():
|
|
32
|
+
return repo_secret
|
|
33
|
+
return get_google_data_dir(taskrc) / "client_secret.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_token_path(account_id, taskrc=None):
|
|
37
|
+
return get_google_data_dir(taskrc) / f"{account_id}.token.json"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _safe_account_id(email):
|
|
41
|
+
return email.replace("@", "_").replace(".", "_")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_credentials(taskrc=None):
|
|
45
|
+
secrets_path = get_client_secrets_path(taskrc)
|
|
46
|
+
if not secrets_path.exists():
|
|
47
|
+
raise FileNotFoundError(f"Missing Google OAuth client secrets: {secrets_path}")
|
|
48
|
+
|
|
49
|
+
token_dir = get_google_data_dir(taskrc)
|
|
50
|
+
token_dir.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
token_path = token_dir / "current.token.json"
|
|
52
|
+
creds = None
|
|
53
|
+
if token_path.exists():
|
|
54
|
+
creds = Credentials.from_authorized_user_file(token_path, SCOPES)
|
|
55
|
+
if creds and creds.expired and creds.refresh_token:
|
|
56
|
+
creds.refresh(Request())
|
|
57
|
+
if not creds or not creds.valid:
|
|
58
|
+
flow = InstalledAppFlow.from_client_secrets_file(str(secrets_path), SCOPES)
|
|
59
|
+
with warnings.catch_warnings():
|
|
60
|
+
warnings.filterwarnings("ignore", message=r'^Scope has changed from .*')
|
|
61
|
+
creds = flow.run_local_server(port=0)
|
|
62
|
+
token_path.write_text(creds.to_json())
|
|
63
|
+
return creds
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_google_user_email(credentials):
|
|
67
|
+
response = requests.get(
|
|
68
|
+
"https://openidconnect.googleapis.com/v1/userinfo",
|
|
69
|
+
headers={"Authorization": f"Bearer {credentials.token}"},
|
|
70
|
+
timeout=30,
|
|
71
|
+
)
|
|
72
|
+
response.raise_for_status()
|
|
73
|
+
payload = response.json()
|
|
74
|
+
return payload["email"], payload.get("name", payload["email"])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def list_calendars(credentials):
|
|
78
|
+
service = build("calendar", "v3", credentials=credentials, cache_discovery=False)
|
|
79
|
+
result = service.calendarList().list().execute()
|
|
80
|
+
return result.get("items", [])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def google_calendar_to_dict(calendar_id, token_path, days_ahead, all_day=True):
|
|
84
|
+
token_path = Path(token_path).expanduser()
|
|
85
|
+
if not token_path.exists():
|
|
86
|
+
raise FileNotFoundError(f"Missing Google OAuth token: {token_path}")
|
|
87
|
+
|
|
88
|
+
credentials = Credentials.from_authorized_user_file(str(token_path), SCOPES)
|
|
89
|
+
if credentials.expired and credentials.refresh_token:
|
|
90
|
+
credentials.refresh(Request())
|
|
91
|
+
token_path.write_text(credentials.to_json())
|
|
92
|
+
if not credentials.valid:
|
|
93
|
+
raise RuntimeError(f"Invalid Google OAuth token: {token_path}")
|
|
94
|
+
|
|
95
|
+
service = build("calendar", "v3", credentials=credentials, cache_discovery=False)
|
|
96
|
+
timezone = datetime.now().astimezone().tzinfo
|
|
97
|
+
start = datetime.combine(datetime.now().date(), time.min, timezone)
|
|
98
|
+
end = start + timedelta(days=days_ahead + 1)
|
|
99
|
+
params = {
|
|
100
|
+
"calendarId": calendar_id,
|
|
101
|
+
"timeMin": start.isoformat(),
|
|
102
|
+
"timeMax": end.isoformat(),
|
|
103
|
+
"singleEvents": True,
|
|
104
|
+
"orderBy": "startTime",
|
|
105
|
+
}
|
|
106
|
+
events = []
|
|
107
|
+
while True:
|
|
108
|
+
result = service.events().list(**params).execute()
|
|
109
|
+
for item in result.get("items", []):
|
|
110
|
+
event_start = item.get("start", {})
|
|
111
|
+
event_end = item.get("end", {})
|
|
112
|
+
if "dateTime" in event_start and "dateTime" in event_end:
|
|
113
|
+
events.append(
|
|
114
|
+
{"start": event_start["dateTime"], "end": event_end["dateTime"]}
|
|
115
|
+
)
|
|
116
|
+
elif all_day and "date" in event_start and "date" in event_end:
|
|
117
|
+
events.append(
|
|
118
|
+
{
|
|
119
|
+
"start": datetime.fromisoformat(event_start["date"]).isoformat(),
|
|
120
|
+
"end": datetime.fromisoformat(event_end["date"]).isoformat(),
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
page_token = result.get("nextPageToken")
|
|
124
|
+
if not page_token:
|
|
125
|
+
break
|
|
126
|
+
params["pageToken"] = page_token
|
|
127
|
+
return events
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def select_from_list(items, title_key="summary"):
|
|
131
|
+
for idx, item in enumerate(items, start=1):
|
|
132
|
+
print(f"{idx}. {item.get(title_key, item.get('id'))}")
|
|
133
|
+
while True:
|
|
134
|
+
choice = input("Select one calendar: ").strip()
|
|
135
|
+
if choice.isdigit() and 1 <= int(choice) <= len(items):
|
|
136
|
+
return items[int(choice) - 1]
|
|
137
|
+
print("Invalid selection")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def render_calendar_config(account_id, calendar, taskrc=None):
|
|
141
|
+
token_path = get_token_path(account_id, taskrc)
|
|
142
|
+
return f'''[calendars."{account_id}.{calendar["id"]}"]
|
|
143
|
+
url = "google://{calendar["id"]}"
|
|
144
|
+
provider = "google"
|
|
145
|
+
account = "{account_id}"
|
|
146
|
+
calendar_id = "{calendar["id"]}"
|
|
147
|
+
token_path = "{token_path}"
|
|
148
|
+
event_all_day_is_blocking = true
|
|
149
|
+
expiration = 0.25
|
|
150
|
+
'''
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def add_google_calendar(taskrc=None):
|
|
154
|
+
credentials = load_credentials(taskrc)
|
|
155
|
+
email, name = get_google_user_email(credentials)
|
|
156
|
+
account_id = _safe_account_id(email)
|
|
157
|
+
token_path = get_token_path(account_id, taskrc)
|
|
158
|
+
token_path.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
token_path.write_text(credentials.to_json())
|
|
160
|
+
|
|
161
|
+
calendars = list_calendars(credentials)
|
|
162
|
+
if not calendars:
|
|
163
|
+
raise RuntimeError("No Google calendars found for this account")
|
|
164
|
+
calendar = select_from_list(calendars)
|
|
165
|
+
print(render_calendar_config(account_id, calendar, taskrc=taskrc))
|
|
166
|
+
return {
|
|
167
|
+
"account_id": account_id,
|
|
168
|
+
"email": email,
|
|
169
|
+
"name": name,
|
|
170
|
+
"calendar": calendar,
|
|
171
|
+
"token_path": str(token_path),
|
|
172
|
+
}
|
|
@@ -535,7 +535,7 @@ def update_tasks_with_scheduling_info(task_info, verbose, taskrc):
|
|
|
535
535
|
"modify",
|
|
536
536
|
f"scheduled:{start_date}",
|
|
537
537
|
f"completion_date:{end_date}",
|
|
538
|
-
f
|
|
538
|
+
f"scheduling:{scheduling_note.strip()}",
|
|
539
539
|
],
|
|
540
540
|
stdout=subprocess.DEVNULL,
|
|
541
541
|
stderr=subprocess.DEVNULL,
|
|
@@ -187,7 +187,7 @@ def get_tasks(config, tasks, year, month, day):
|
|
|
187
187
|
if m:
|
|
188
188
|
due = task.get("due", "")
|
|
189
189
|
if due:
|
|
190
|
-
due =
|
|
190
|
+
due = parse_taskwarrior_timestamp(due)
|
|
191
191
|
due = due - datetime.today()
|
|
192
192
|
valid_tasks.append(
|
|
193
193
|
{
|
|
@@ -219,7 +219,7 @@ def get_taskwarrior_date(date, _retry=True, taskrc=None):
|
|
|
219
219
|
)
|
|
220
220
|
date = date.stdout.strip()
|
|
221
221
|
try:
|
|
222
|
-
date =
|
|
222
|
+
date = parse_taskwarrior_timestamp(date)
|
|
223
223
|
except Exception as _:
|
|
224
224
|
if _retry:
|
|
225
225
|
return get_taskwarrior_date("today+" + date, False, taskrc)
|
|
@@ -247,7 +247,7 @@ def tostring(value):
|
|
|
247
247
|
return value.strftime("%Y-%m-%d %H:%M")
|
|
248
248
|
elif isinstance(value, str):
|
|
249
249
|
try:
|
|
250
|
-
date =
|
|
250
|
+
date = parse_taskwarrior_timestamp(value)
|
|
251
251
|
return date.strftime("%Y-%m-%d %H:%M")
|
|
252
252
|
except ValueError:
|
|
253
253
|
return value
|
|
@@ -275,7 +275,7 @@ def get_unplanned_tasks(config, tasks, taskrc=None):
|
|
|
275
275
|
task
|
|
276
276
|
for task in tasks
|
|
277
277
|
if not task.get("due")
|
|
278
|
-
or
|
|
278
|
+
or parse_taskwarrior_timestamp(task["due"]) <= due_limit
|
|
279
279
|
]
|
|
280
280
|
|
|
281
281
|
|
|
@@ -298,6 +298,15 @@ def generate_report(config, constraint, verbose=False, force_update=False, taskr
|
|
|
298
298
|
display_tasks_table(console, config, this_day_tasks)
|
|
299
299
|
|
|
300
300
|
|
|
301
|
+
def parse_taskwarrior_timestamp(value):
|
|
302
|
+
for fmt in ("%Y%m%dT%H%M%SZ", "%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S"):
|
|
303
|
+
try:
|
|
304
|
+
return datetime.strptime(value, fmt)
|
|
305
|
+
except ValueError:
|
|
306
|
+
pass
|
|
307
|
+
raise ValueError(value)
|
|
308
|
+
|
|
309
|
+
|
|
301
310
|
def fetch_tasks(taskrc=None):
|
|
302
311
|
"""Fetch tasks from the task manager and return them as a JSON object."""
|
|
303
312
|
from taskcheck.common import get_task_env
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: taskcheck
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Automatic scheduler for taskwarrior
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
Requires-Dist: appdirs>=1.4.4
|
|
8
|
+
Requires-Dist: google-api-python-client>=2.198.0
|
|
9
|
+
Requires-Dist: google-auth>=2.56.2
|
|
10
|
+
Requires-Dist: google-auth-oauthlib>=1.2.2
|
|
8
11
|
Requires-Dist: icalendar>=6.0.1
|
|
9
12
|
Requires-Dist: random-unicode-emoji>=2.8
|
|
10
13
|
Requires-Dist: requests>=2.32.3
|
|
@@ -3,6 +3,7 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
taskcheck/__main__.py
|
|
5
5
|
taskcheck/common.py
|
|
6
|
+
taskcheck/google_calendar.py
|
|
6
7
|
taskcheck/ical.py
|
|
7
8
|
taskcheck/install.py
|
|
8
9
|
taskcheck/parallel.py
|
|
@@ -14,6 +15,7 @@ taskcheck.egg-info/entry_points.txt
|
|
|
14
15
|
taskcheck.egg-info/requires.txt
|
|
15
16
|
taskcheck.egg-info/top_level.txt
|
|
16
17
|
tests/test_common.py
|
|
18
|
+
tests/test_google_calendar.py
|
|
17
19
|
tests/test_ical.py
|
|
18
20
|
tests/test_install.py
|
|
19
21
|
tests/test_main.py
|
|
@@ -13,7 +13,8 @@ from taskcheck.common import (
|
|
|
13
13
|
get_long_range_time_map,
|
|
14
14
|
get_tasks,
|
|
15
15
|
get_calendars,
|
|
16
|
-
get_task_env
|
|
16
|
+
get_task_env,
|
|
17
|
+
mark_end_date,
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
|
|
@@ -34,6 +35,23 @@ class TestTimeConversions:
|
|
|
34
35
|
assert time_to_decimal(time(14, 15)) == 14.25
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
class TestTaskUpdates:
|
|
39
|
+
@patch("taskcheck.common.subprocess.run")
|
|
40
|
+
def test_mark_end_date_passes_unquoted_scheduling_value(self, mock_run):
|
|
41
|
+
mark_end_date(
|
|
42
|
+
None,
|
|
43
|
+
"2026-07-29",
|
|
44
|
+
"2026-07-28",
|
|
45
|
+
"2026-07-28 - PT1H\n2026-07-29 - PT1H",
|
|
46
|
+
1,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
command = mock_run.call_args.args[0]
|
|
50
|
+
assert command[-1] == (
|
|
51
|
+
"scheduling:2026-07-28 - PT1H\n2026-07-29 - PT1H"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
37
55
|
class TestDurationConversions:
|
|
38
56
|
def test_pdth_to_hours(self):
|
|
39
57
|
assert pdth_to_hours("P1H") == 1.0
|
|
@@ -132,6 +150,31 @@ class TestGetCalendars:
|
|
|
132
150
|
assert calendars[0] == sample_calendar_events
|
|
133
151
|
mock_ical.assert_called_once()
|
|
134
152
|
|
|
153
|
+
@patch("taskcheck.google_calendar.google_calendar_to_dict")
|
|
154
|
+
@patch("taskcheck.common.ical_to_dict")
|
|
155
|
+
def test_get_google_calendars(
|
|
156
|
+
self, mock_ical, mock_google, sample_config, sample_calendar_events
|
|
157
|
+
):
|
|
158
|
+
sample_config["calendars"] = {
|
|
159
|
+
"google": {
|
|
160
|
+
"url": "google://primary",
|
|
161
|
+
"provider": "google",
|
|
162
|
+
"calendar_id": "primary",
|
|
163
|
+
"token_path": "/tmp/google.token.json",
|
|
164
|
+
"event_all_day_is_blocking": True,
|
|
165
|
+
"expiration": 0.25,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
mock_google.return_value = sample_calendar_events
|
|
169
|
+
|
|
170
|
+
calendars = get_calendars(sample_config)
|
|
171
|
+
|
|
172
|
+
assert calendars == [sample_calendar_events]
|
|
173
|
+
mock_google.assert_called_once_with(
|
|
174
|
+
"primary", "/tmp/google.token.json", 7, all_day=True
|
|
175
|
+
)
|
|
176
|
+
mock_ical.assert_not_called()
|
|
177
|
+
|
|
135
178
|
|
|
136
179
|
class TestEnvironmentVariables:
|
|
137
180
|
def test_get_task_env_sets_both_variables(self, test_taskrc):
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from unittest.mock import Mock, patch
|
|
3
|
+
|
|
4
|
+
from taskcheck.google_calendar import (
|
|
5
|
+
_safe_account_id,
|
|
6
|
+
get_client_secrets_path,
|
|
7
|
+
google_calendar_to_dict,
|
|
8
|
+
render_calendar_config,
|
|
9
|
+
)
|
|
10
|
+
from taskcheck.__main__ import main
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_safe_account_id():
|
|
14
|
+
assert _safe_account_id("foo.bar@example.com") == "foo_bar_example_com"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_get_client_secrets_path_prefers_repo_root(tmp_path, monkeypatch):
|
|
18
|
+
secret = tmp_path / "client_secret.json"
|
|
19
|
+
secret.write_text("{}")
|
|
20
|
+
monkeypatch.chdir(tmp_path)
|
|
21
|
+
assert get_client_secrets_path("/tmp/task") == secret
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@patch("taskcheck.google_calendar.get_token_path")
|
|
25
|
+
def test_render_calendar_config(mock_token_path):
|
|
26
|
+
mock_token_path.return_value = "/tmp/task/google/foo.token.json"
|
|
27
|
+
cfg = render_calendar_config("foo", {"id": "abc123"}, taskrc="/tmp/task")
|
|
28
|
+
assert '[calendars."foo.abc123"]' in cfg
|
|
29
|
+
assert "calendar_id = \"abc123\"" in cfg
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@patch("taskcheck.google_calendar.build")
|
|
33
|
+
@patch("taskcheck.google_calendar.Credentials.from_authorized_user_file")
|
|
34
|
+
def test_google_calendar_to_dict(mock_credentials, mock_build, tmp_path):
|
|
35
|
+
token_path = tmp_path / "token.json"
|
|
36
|
+
token_path.write_text("{}")
|
|
37
|
+
mock_credentials.return_value = Mock(valid=True, expired=False)
|
|
38
|
+
events = Mock()
|
|
39
|
+
events.list.return_value.execute.return_value = {
|
|
40
|
+
"items": [
|
|
41
|
+
{
|
|
42
|
+
"start": {"dateTime": "2025-01-02T09:00:00+01:00"},
|
|
43
|
+
"end": {"dateTime": "2025-01-02T10:00:00+01:00"},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"start": {"date": "2025-01-03"},
|
|
47
|
+
"end": {"date": "2025-01-04"},
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
mock_build.return_value.events.return_value = events
|
|
52
|
+
|
|
53
|
+
result = google_calendar_to_dict("primary", token_path, 7, all_day=True)
|
|
54
|
+
|
|
55
|
+
assert result == [
|
|
56
|
+
{
|
|
57
|
+
"start": "2025-01-02T09:00:00+01:00",
|
|
58
|
+
"end": "2025-01-02T10:00:00+01:00",
|
|
59
|
+
},
|
|
60
|
+
{"start": "2025-01-03T00:00:00", "end": "2025-01-04T00:00:00"},
|
|
61
|
+
]
|
|
62
|
+
events.list.assert_called_once()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@patch("taskcheck.google_calendar.add_google_calendar")
|
|
66
|
+
@patch("taskcheck.__main__.arg_parser.parse_args")
|
|
67
|
+
def test_main_add_google_calendar(mock_args, mock_add):
|
|
68
|
+
mock_args.return_value = Mock(
|
|
69
|
+
install=False,
|
|
70
|
+
add_google_calendar=True,
|
|
71
|
+
schedule=False,
|
|
72
|
+
report=None,
|
|
73
|
+
verbose=False,
|
|
74
|
+
force_update=False,
|
|
75
|
+
taskrc="/tmp/task",
|
|
76
|
+
urgency_weight=None,
|
|
77
|
+
dry_run=False,
|
|
78
|
+
auto_adjust_urgency=True,
|
|
79
|
+
)
|
|
80
|
+
main()
|
|
81
|
+
mock_add.assert_called_once_with(taskrc="/tmp/task")
|
|
@@ -538,4 +538,6 @@ class TestAutoAdjustUrgency:
|
|
|
538
538
|
from taskcheck.parallel import update_tasks_with_scheduling_info
|
|
539
539
|
update_tasks_with_scheduling_info(task_info, verbose=False, taskrc="/tmp/taskrc")
|
|
540
540
|
mock_run.assert_called_once()
|
|
541
|
+
command = mock_run.call_args.args[0]
|
|
542
|
+
assert command[-1] == "scheduling:2023-12-05 - PT2H"
|
|
541
543
|
assert mock_print.called
|
|
@@ -12,6 +12,7 @@ from taskcheck.report import (
|
|
|
12
12
|
tostring,
|
|
13
13
|
get_unplanned_tasks,
|
|
14
14
|
fetch_tasks,
|
|
15
|
+
parse_taskwarrior_timestamp,
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
|
|
@@ -152,10 +153,19 @@ class TestStringFormatting:
|
|
|
152
153
|
dt = datetime(2023, 12, 5, 14, 30, 0)
|
|
153
154
|
assert tostring(dt) == "2023-12-05 14:30"
|
|
154
155
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
@pytest.mark.parametrize(
|
|
157
|
+
("timestamp", "expected"),
|
|
158
|
+
[
|
|
159
|
+
("20231205T143000Z", datetime(2023, 12, 5, 14, 30)),
|
|
160
|
+
("2023-12-05T14:30:00Z", datetime(2023, 12, 5, 14, 30)),
|
|
161
|
+
],
|
|
162
|
+
)
|
|
163
|
+
def test_parse_taskwarrior_timestamp_supports_v2_and_v3(self, timestamp, expected):
|
|
164
|
+
assert parse_taskwarrior_timestamp(timestamp) == expected
|
|
165
|
+
|
|
166
|
+
@pytest.mark.parametrize("timestamp", ["20231205T143000Z", "2023-12-05T14:30:00Z"])
|
|
167
|
+
def test_tostring_taskwarrior_date(self, timestamp):
|
|
168
|
+
assert tostring(timestamp) == "2023-12-05 14:30"
|
|
159
169
|
|
|
160
170
|
def test_tostring_regular_string(self):
|
|
161
171
|
assert tostring("hello") == "hello"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|