taskcheck 1.1.2__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.2/taskcheck.egg-info → taskcheck-1.2.0}/PKG-INFO +4 -1
- {taskcheck-1.1.2 → taskcheck-1.2.0}/pyproject.toml +4 -1
- {taskcheck-1.1.2 → taskcheck-1.2.0}/taskcheck/__main__.py +11 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/taskcheck/common.py +25 -10
- taskcheck-1.2.0/taskcheck/google_calendar.py +172 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/taskcheck/report.py +13 -4
- {taskcheck-1.1.2 → taskcheck-1.2.0/taskcheck.egg-info}/PKG-INFO +4 -1
- {taskcheck-1.1.2 → taskcheck-1.2.0}/taskcheck.egg-info/SOURCES.txt +2 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/taskcheck.egg-info/requires.txt +3 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/tests/test_common.py +25 -0
- taskcheck-1.2.0/tests/test_google_calendar.py +81 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/tests/test_report.py +14 -4
- {taskcheck-1.1.2 → taskcheck-1.2.0}/LICENSE +0 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/README.md +0 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/setup.cfg +0 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/taskcheck/ical.py +0 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/taskcheck/install.py +0 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/taskcheck/parallel.py +0 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/taskcheck.egg-info/dependency_links.txt +0 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/taskcheck.egg-info/entry_points.txt +0 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/taskcheck.egg-info/top_level.txt +0 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/tests/test_ical.py +0 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/tests/test_install.py +0 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/tests/test_main.py +0 -0
- {taskcheck-1.1.2 → taskcheck-1.2.0}/tests/test_parallel.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(
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
@@ -150,6 +150,31 @@ class TestGetCalendars:
|
|
|
150
150
|
assert calendars[0] == sample_calendar_events
|
|
151
151
|
mock_ical.assert_called_once()
|
|
152
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
|
+
|
|
153
178
|
|
|
154
179
|
class TestEnvironmentVariables:
|
|
155
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")
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|