google-api-client-wrapper 1.0.0__py3-none-any.whl
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.
- google_api_client_wrapper-1.0.0.dist-info/METADATA +103 -0
- google_api_client_wrapper-1.0.0.dist-info/RECORD +39 -0
- google_api_client_wrapper-1.0.0.dist-info/WHEEL +5 -0
- google_api_client_wrapper-1.0.0.dist-info/licenses/LICENSE +21 -0
- google_api_client_wrapper-1.0.0.dist-info/top_level.txt +1 -0
- google_client/__init__.py +6 -0
- google_client/services/__init__.py +13 -0
- google_client/services/calendar/__init__.py +14 -0
- google_client/services/calendar/api_service.py +454 -0
- google_client/services/calendar/constants.py +48 -0
- google_client/services/calendar/exceptions.py +35 -0
- google_client/services/calendar/query_builder.py +314 -0
- google_client/services/calendar/types.py +403 -0
- google_client/services/calendar/utils.py +338 -0
- google_client/services/drive/__init__.py +13 -0
- google_client/services/drive/api_service.py +1133 -0
- google_client/services/drive/constants.py +37 -0
- google_client/services/drive/exceptions.py +60 -0
- google_client/services/drive/query_builder.py +385 -0
- google_client/services/drive/types.py +242 -0
- google_client/services/drive/utils.py +392 -0
- google_client/services/gmail/__init__.py +16 -0
- google_client/services/gmail/api_service.py +715 -0
- google_client/services/gmail/constants.py +6 -0
- google_client/services/gmail/exceptions.py +45 -0
- google_client/services/gmail/query_builder.py +408 -0
- google_client/services/gmail/types.py +285 -0
- google_client/services/gmail/utils.py +426 -0
- google_client/services/tasks/__init__.py +12 -0
- google_client/services/tasks/api_service.py +561 -0
- google_client/services/tasks/constants.py +32 -0
- google_client/services/tasks/exceptions.py +35 -0
- google_client/services/tasks/query_builder.py +324 -0
- google_client/services/tasks/types.py +156 -0
- google_client/services/tasks/utils.py +224 -0
- google_client/user_client.py +208 -0
- google_client/utils/__init__.py +0 -0
- google_client/utils/datetime.py +144 -0
- google_client/utils/validation.py +71 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
from datetime import datetime, date, timedelta
|
|
2
|
+
from typing import Optional, List, TYPE_CHECKING
|
|
3
|
+
from .constants import MAX_RESULTS_LIMIT, DEFAULT_MAX_RESULTS, DEFAULT_TASK_LIST_ID
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .types import Task
|
|
7
|
+
from .api_service import TasksApiService
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TaskQueryBuilder:
|
|
11
|
+
"""
|
|
12
|
+
Builder pattern for constructing Google Tasks queries with a fluent API.
|
|
13
|
+
Provides a clean, readable way to build complex task queries.
|
|
14
|
+
|
|
15
|
+
Example usage:
|
|
16
|
+
tasks = (Task.query()
|
|
17
|
+
.limit(50)
|
|
18
|
+
.due_before(end_date)
|
|
19
|
+
.completed_after(start_date)
|
|
20
|
+
.show_completed(True)
|
|
21
|
+
.execute())
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, api_service: "TasksApiService"):
|
|
25
|
+
self._api_service = api_service
|
|
26
|
+
self._max_results: Optional[int] = DEFAULT_MAX_RESULTS
|
|
27
|
+
self._completed_max: Optional[datetime] = None
|
|
28
|
+
self._completed_min: Optional[datetime] = None
|
|
29
|
+
self._due_max: Optional[datetime] = None
|
|
30
|
+
self._due_min: Optional[datetime] = None
|
|
31
|
+
self._show_completed: Optional[bool] = None
|
|
32
|
+
self._show_hidden: Optional[bool] = None
|
|
33
|
+
self._task_list_id: str = DEFAULT_TASK_LIST_ID
|
|
34
|
+
|
|
35
|
+
def limit(self, count: int) -> "TaskQueryBuilder":
|
|
36
|
+
"""
|
|
37
|
+
Set the maximum number of tasks to retrieve.
|
|
38
|
+
Args:
|
|
39
|
+
count: Maximum number of tasks (1-100)
|
|
40
|
+
Returns:
|
|
41
|
+
Self for method chaining
|
|
42
|
+
"""
|
|
43
|
+
if count < 1 or count > MAX_RESULTS_LIMIT:
|
|
44
|
+
raise ValueError(f"Limit must be between 1 and {MAX_RESULTS_LIMIT}")
|
|
45
|
+
self._max_results = count
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
def completed_after(self, min_date: datetime) -> "TaskQueryBuilder":
|
|
49
|
+
"""
|
|
50
|
+
Filter tasks completed after the specified date.
|
|
51
|
+
Args:
|
|
52
|
+
min_date: Minimum completion date (RFC 3339 timestamp)
|
|
53
|
+
Returns:
|
|
54
|
+
Self for method chaining
|
|
55
|
+
"""
|
|
56
|
+
self._completed_min = min_date
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def completed_before(self, max_date: datetime) -> "TaskQueryBuilder":
|
|
60
|
+
"""
|
|
61
|
+
Filter tasks completed before the specified date.
|
|
62
|
+
Args:
|
|
63
|
+
max_date: Maximum completion date (RFC 3339 timestamp)
|
|
64
|
+
Returns:
|
|
65
|
+
Self for method chaining
|
|
66
|
+
"""
|
|
67
|
+
self._completed_max = max_date
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def completed_in_range(self, min_date: datetime, max_date: datetime) -> "TaskQueryBuilder":
|
|
71
|
+
"""
|
|
72
|
+
Filter tasks completed within the specified date range.
|
|
73
|
+
Args:
|
|
74
|
+
min_date: Minimum completion date
|
|
75
|
+
max_date: Maximum completion date
|
|
76
|
+
Returns:
|
|
77
|
+
Self for method chaining
|
|
78
|
+
"""
|
|
79
|
+
if min_date >= max_date:
|
|
80
|
+
raise ValueError("Start date must be before end date")
|
|
81
|
+
self._completed_min = min_date
|
|
82
|
+
self._completed_max = max_date
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
def due_after(self, min_date: datetime) -> "TaskQueryBuilder":
|
|
86
|
+
"""
|
|
87
|
+
Filter tasks due after the specified date.
|
|
88
|
+
Args:
|
|
89
|
+
min_date: Minimum due date (RFC 3339 timestamp)
|
|
90
|
+
Returns:
|
|
91
|
+
Self for method chaining
|
|
92
|
+
"""
|
|
93
|
+
self._due_min = min_date
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def due_before(self, max_date: datetime) -> "TaskQueryBuilder":
|
|
97
|
+
"""
|
|
98
|
+
Filter tasks due before the specified date.
|
|
99
|
+
Args:
|
|
100
|
+
max_date: Maximum due date (RFC 3339 timestamp)
|
|
101
|
+
Returns:
|
|
102
|
+
Self for method chaining
|
|
103
|
+
"""
|
|
104
|
+
self._due_max = max_date
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def due_in_range(self, min_date: datetime, max_date: datetime) -> "TaskQueryBuilder":
|
|
108
|
+
"""
|
|
109
|
+
Filter tasks due within the specified date range.
|
|
110
|
+
Args:
|
|
111
|
+
min_date: Minimum due date
|
|
112
|
+
max_date: Maximum due date
|
|
113
|
+
Returns:
|
|
114
|
+
Self for method chaining
|
|
115
|
+
"""
|
|
116
|
+
if min_date >= max_date:
|
|
117
|
+
raise ValueError("Start date must be before end date")
|
|
118
|
+
self._due_min = min_date
|
|
119
|
+
self._due_max = max_date
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
def show_completed(self, show: bool = True) -> "TaskQueryBuilder":
|
|
123
|
+
"""
|
|
124
|
+
Include or exclude completed tasks in results.
|
|
125
|
+
Args:
|
|
126
|
+
show: Whether to include completed tasks
|
|
127
|
+
Returns:
|
|
128
|
+
Self for method chaining
|
|
129
|
+
"""
|
|
130
|
+
self._show_completed = show
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
def show_hidden(self, show: bool = True) -> "TaskQueryBuilder":
|
|
134
|
+
"""
|
|
135
|
+
Include or exclude hidden tasks in results.
|
|
136
|
+
Args:
|
|
137
|
+
show: Whether to include hidden tasks
|
|
138
|
+
Returns:
|
|
139
|
+
Self for method chaining
|
|
140
|
+
"""
|
|
141
|
+
self._show_hidden = show
|
|
142
|
+
return self
|
|
143
|
+
|
|
144
|
+
def in_task_list(self, task_list_id: str) -> "TaskQueryBuilder":
|
|
145
|
+
"""
|
|
146
|
+
Specify which task list to query.
|
|
147
|
+
Args:
|
|
148
|
+
task_list_id: Task list identifier
|
|
149
|
+
Returns:
|
|
150
|
+
Self for method chaining
|
|
151
|
+
"""
|
|
152
|
+
self._task_list_id = task_list_id
|
|
153
|
+
return self
|
|
154
|
+
|
|
155
|
+
# Convenience date methods
|
|
156
|
+
def due_today(self) -> "TaskQueryBuilder":
|
|
157
|
+
"""
|
|
158
|
+
Filter to tasks due today.
|
|
159
|
+
Returns:
|
|
160
|
+
Self for method chaining
|
|
161
|
+
"""
|
|
162
|
+
today = date.today()
|
|
163
|
+
start_of_day = datetime.combine(today, datetime.min.time())
|
|
164
|
+
end_of_day = start_of_day + timedelta(days=1)
|
|
165
|
+
return self.due_in_range(start_of_day, end_of_day)
|
|
166
|
+
|
|
167
|
+
def due_tomorrow(self) -> "TaskQueryBuilder":
|
|
168
|
+
"""
|
|
169
|
+
Filter to tasks due tomorrow.
|
|
170
|
+
Returns:
|
|
171
|
+
Self for method chaining
|
|
172
|
+
"""
|
|
173
|
+
tomorrow = date.today() + timedelta(days=1)
|
|
174
|
+
start_of_day = datetime.combine(tomorrow, datetime.min.time())
|
|
175
|
+
end_of_day = start_of_day + timedelta(days=1)
|
|
176
|
+
return self.due_in_range(start_of_day, end_of_day)
|
|
177
|
+
|
|
178
|
+
def due_this_week(self) -> "TaskQueryBuilder":
|
|
179
|
+
"""
|
|
180
|
+
Filter to tasks due this week (Monday to Sunday).
|
|
181
|
+
Returns:
|
|
182
|
+
Self for method chaining
|
|
183
|
+
"""
|
|
184
|
+
today = date.today()
|
|
185
|
+
days_since_monday = today.weekday()
|
|
186
|
+
monday = today - timedelta(days=days_since_monday)
|
|
187
|
+
sunday = monday + timedelta(days=6)
|
|
188
|
+
|
|
189
|
+
start_of_week = datetime.combine(monday, datetime.min.time())
|
|
190
|
+
end_of_week = datetime.combine(sunday, datetime.max.time())
|
|
191
|
+
return self.due_in_range(start_of_week, end_of_week)
|
|
192
|
+
|
|
193
|
+
def due_next_week(self) -> "TaskQueryBuilder":
|
|
194
|
+
"""
|
|
195
|
+
Filter to tasks due next week (Monday to Sunday).
|
|
196
|
+
Returns:
|
|
197
|
+
Self for method chaining
|
|
198
|
+
"""
|
|
199
|
+
today = date.today()
|
|
200
|
+
days_since_monday = today.weekday()
|
|
201
|
+
next_monday = today + timedelta(days=(7 - days_since_monday))
|
|
202
|
+
next_sunday = next_monday + timedelta(days=6)
|
|
203
|
+
|
|
204
|
+
start_of_week = datetime.combine(next_monday, datetime.min.time())
|
|
205
|
+
end_of_week = datetime.combine(next_sunday, datetime.max.time())
|
|
206
|
+
return self.due_in_range(start_of_week, end_of_week)
|
|
207
|
+
|
|
208
|
+
def due_next_days(self, days: int) -> "TaskQueryBuilder":
|
|
209
|
+
"""
|
|
210
|
+
Filter to tasks due in the next N days.
|
|
211
|
+
Args:
|
|
212
|
+
days: Number of days from today
|
|
213
|
+
Returns:
|
|
214
|
+
Self for method chaining
|
|
215
|
+
"""
|
|
216
|
+
if days < 1:
|
|
217
|
+
raise ValueError("Days must be positive")
|
|
218
|
+
|
|
219
|
+
today = date.today()
|
|
220
|
+
start = datetime.combine(today, datetime.min.time())
|
|
221
|
+
end = datetime.combine(today + timedelta(days=days + 1), datetime.min.time())
|
|
222
|
+
return self.due_in_range(start, end)
|
|
223
|
+
|
|
224
|
+
def overdue(self) -> "TaskQueryBuilder":
|
|
225
|
+
"""
|
|
226
|
+
Filter to tasks that are overdue (due date in the past and not completed).
|
|
227
|
+
Returns:
|
|
228
|
+
Self for method chaining
|
|
229
|
+
"""
|
|
230
|
+
today = datetime.combine(date.today(), datetime.min.time())
|
|
231
|
+
return self.due_before(today).show_completed(False)
|
|
232
|
+
|
|
233
|
+
def completed_today(self) -> "TaskQueryBuilder":
|
|
234
|
+
"""
|
|
235
|
+
Filter to tasks completed today.
|
|
236
|
+
Returns:
|
|
237
|
+
Self for method chaining
|
|
238
|
+
"""
|
|
239
|
+
today = date.today()
|
|
240
|
+
start_of_day = datetime.combine(today, datetime.min.time())
|
|
241
|
+
end_of_day = start_of_day + timedelta(days=1)
|
|
242
|
+
return self.completed_in_range(start_of_day, end_of_day)
|
|
243
|
+
|
|
244
|
+
def completed_this_week(self) -> "TaskQueryBuilder":
|
|
245
|
+
"""
|
|
246
|
+
Filter to tasks completed this week (Monday to Sunday).
|
|
247
|
+
Returns:
|
|
248
|
+
Self for method chaining
|
|
249
|
+
"""
|
|
250
|
+
today = date.today()
|
|
251
|
+
days_since_monday = today.weekday()
|
|
252
|
+
monday = today - timedelta(days=days_since_monday)
|
|
253
|
+
sunday = monday + timedelta(days=6)
|
|
254
|
+
|
|
255
|
+
start_of_week = datetime.combine(monday, datetime.min.time())
|
|
256
|
+
end_of_week = datetime.combine(sunday, datetime.max.time())
|
|
257
|
+
return self.completed_in_range(start_of_week, end_of_week)
|
|
258
|
+
|
|
259
|
+
def completed_last_days(self, days: int) -> "TaskQueryBuilder":
|
|
260
|
+
"""
|
|
261
|
+
Filter to tasks completed in the last N days.
|
|
262
|
+
Args:
|
|
263
|
+
days: Number of days back to search
|
|
264
|
+
Returns:
|
|
265
|
+
Self for method chaining
|
|
266
|
+
"""
|
|
267
|
+
if days < 1:
|
|
268
|
+
raise ValueError("Days must be positive")
|
|
269
|
+
|
|
270
|
+
today = date.today()
|
|
271
|
+
start = datetime.combine(today - timedelta(days=days), datetime.min.time())
|
|
272
|
+
end = datetime.combine(today, datetime.max.time())
|
|
273
|
+
return self.completed_in_range(start, end)
|
|
274
|
+
|
|
275
|
+
def execute(self) -> List["Task"]:
|
|
276
|
+
"""
|
|
277
|
+
Execute the query and return the results.
|
|
278
|
+
Returns:
|
|
279
|
+
List of Task objects matching the criteria
|
|
280
|
+
Raises:
|
|
281
|
+
ValueError: If query parameters are invalid
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
# Use the service layer implementation
|
|
285
|
+
tasks = self._api_service.list_tasks(
|
|
286
|
+
task_list_id=self._task_list_id,
|
|
287
|
+
max_results=self._max_results,
|
|
288
|
+
completed_min=self._completed_min,
|
|
289
|
+
completed_max=self._completed_max,
|
|
290
|
+
due_min=self._due_min,
|
|
291
|
+
due_max=self._due_max,
|
|
292
|
+
show_completed=self._show_completed,
|
|
293
|
+
show_hidden=self._show_hidden
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return tasks
|
|
297
|
+
|
|
298
|
+
def count(self) -> int:
|
|
299
|
+
"""
|
|
300
|
+
Execute the query and return only the count of matching tasks.
|
|
301
|
+
Returns:
|
|
302
|
+
Number of tasks matching the criteria
|
|
303
|
+
"""
|
|
304
|
+
return len(self.execute())
|
|
305
|
+
|
|
306
|
+
def first(self) -> Optional["Task"]:
|
|
307
|
+
"""
|
|
308
|
+
Execute the query and return only the first matching task.
|
|
309
|
+
Returns:
|
|
310
|
+
First Task or None if no matches
|
|
311
|
+
"""
|
|
312
|
+
tasks = self.limit(1).execute()
|
|
313
|
+
return tasks[0] if tasks else None
|
|
314
|
+
|
|
315
|
+
def exists(self) -> bool:
|
|
316
|
+
"""
|
|
317
|
+
Check if any tasks match the criteria without retrieving them.
|
|
318
|
+
Returns:
|
|
319
|
+
True if at least one task matches, False otherwise
|
|
320
|
+
"""
|
|
321
|
+
return self.limit(1).count() > 0
|
|
322
|
+
|
|
323
|
+
def __repr__(self):
|
|
324
|
+
return f"TaskQueryBuilder(task_list_id='{self._task_list_id}', limit={self._max_results})"
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from datetime import date, datetime, timedelta
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class TaskList:
|
|
8
|
+
"""
|
|
9
|
+
Represents a Google Task List.
|
|
10
|
+
Args:
|
|
11
|
+
task_list_id: Unique identifier for the task list.
|
|
12
|
+
title: The title of the task list.
|
|
13
|
+
updated: Last modification time.
|
|
14
|
+
"""
|
|
15
|
+
task_list_id: Optional[str] = None
|
|
16
|
+
title: Optional[str] = None
|
|
17
|
+
updated: Optional[datetime] = None
|
|
18
|
+
|
|
19
|
+
def to_dict(self) -> dict:
|
|
20
|
+
"""
|
|
21
|
+
Convert TaskList to dictionary format for Google Tasks API.
|
|
22
|
+
Returns:
|
|
23
|
+
Dictionary representation suitable for API calls.
|
|
24
|
+
"""
|
|
25
|
+
task_list_dict = {}
|
|
26
|
+
if self.task_list_id:
|
|
27
|
+
task_list_dict['id'] = self.task_list_id
|
|
28
|
+
if self.title:
|
|
29
|
+
task_list_dict['title'] = self.title
|
|
30
|
+
if self.updated:
|
|
31
|
+
task_list_dict['updated'] = self.updated.isoformat() + 'Z'
|
|
32
|
+
return task_list_dict
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
return f"TaskList(id={self.task_list_id!r}, title={self.title!r})"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Task:
|
|
40
|
+
"""
|
|
41
|
+
Represents a Google Task.
|
|
42
|
+
Args:
|
|
43
|
+
task_id: Unique identifier for the task.
|
|
44
|
+
title: The title of the task.
|
|
45
|
+
notes: Notes describing the task.
|
|
46
|
+
status: Status of the task ('needsAction' or 'completed').
|
|
47
|
+
due: Due date of the task.
|
|
48
|
+
completed: Completion date of the task.
|
|
49
|
+
updated: Last modification time.
|
|
50
|
+
parent: Parent task identifier.
|
|
51
|
+
position: Position in the task list.
|
|
52
|
+
task_list_id: ID of the task list this task belongs to.
|
|
53
|
+
"""
|
|
54
|
+
task_id: Optional[str] = None
|
|
55
|
+
title: Optional[str] = None
|
|
56
|
+
notes: Optional[str] = None
|
|
57
|
+
status: Optional[str] = "needsAction"
|
|
58
|
+
due: Optional[date] = None
|
|
59
|
+
completed: Optional[date] = None
|
|
60
|
+
updated: Optional[date] = None
|
|
61
|
+
parent: Optional[str] = None
|
|
62
|
+
position: Optional[str] = None
|
|
63
|
+
task_list_id: Optional[str] = None
|
|
64
|
+
|
|
65
|
+
def is_completed(self) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
Check if the task is completed.
|
|
68
|
+
Returns:
|
|
69
|
+
True if the task is completed, False otherwise.
|
|
70
|
+
"""
|
|
71
|
+
return self.status == 'completed'
|
|
72
|
+
|
|
73
|
+
def is_overdue(self) -> bool:
|
|
74
|
+
"""
|
|
75
|
+
Check if the task is overdue.
|
|
76
|
+
Returns:
|
|
77
|
+
True if the task has a due date that has passed and is not completed.
|
|
78
|
+
"""
|
|
79
|
+
if not self.due or self.is_completed():
|
|
80
|
+
return False
|
|
81
|
+
return self.due < date.today()
|
|
82
|
+
|
|
83
|
+
def is_due_today(self) -> bool:
|
|
84
|
+
"""
|
|
85
|
+
Check if the task is due today.
|
|
86
|
+
Returns:
|
|
87
|
+
True if the task is due today, False otherwise.
|
|
88
|
+
"""
|
|
89
|
+
if not self.due:
|
|
90
|
+
return False
|
|
91
|
+
return self.due == date.today()
|
|
92
|
+
|
|
93
|
+
def is_due_soon(self, days: int = 3) -> bool:
|
|
94
|
+
"""
|
|
95
|
+
Check if the task is due within the next N days.
|
|
96
|
+
Args:
|
|
97
|
+
days: Number of days to check ahead (default: 3)
|
|
98
|
+
Returns:
|
|
99
|
+
True if the task is due within the specified days.
|
|
100
|
+
"""
|
|
101
|
+
if not self.due or self.is_completed():
|
|
102
|
+
return False
|
|
103
|
+
today = date.today()
|
|
104
|
+
return today <= self.due <= (today + timedelta(days=days))
|
|
105
|
+
|
|
106
|
+
def has_parent(self) -> bool:
|
|
107
|
+
"""
|
|
108
|
+
Check if the task has a parent task.
|
|
109
|
+
Returns:
|
|
110
|
+
True if the task has a parent, False otherwise.
|
|
111
|
+
"""
|
|
112
|
+
return bool(self.parent)
|
|
113
|
+
|
|
114
|
+
def has_notes(self) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Check if the task has notes.
|
|
117
|
+
Returns:
|
|
118
|
+
True if the task has notes, False otherwise.
|
|
119
|
+
"""
|
|
120
|
+
return bool(self.notes and self.notes.strip())
|
|
121
|
+
|
|
122
|
+
def to_dict(self) -> dict:
|
|
123
|
+
"""
|
|
124
|
+
Convert Task to dictionary format for Google Tasks API.
|
|
125
|
+
Returns:
|
|
126
|
+
Dictionary representation suitable for API calls.
|
|
127
|
+
"""
|
|
128
|
+
task_dict = {}
|
|
129
|
+
if self.task_id:
|
|
130
|
+
task_dict['id'] = self.task_id
|
|
131
|
+
if self.title:
|
|
132
|
+
task_dict['title'] = self.title
|
|
133
|
+
if self.notes:
|
|
134
|
+
task_dict['notes'] = self.notes
|
|
135
|
+
if self.status:
|
|
136
|
+
task_dict['status'] = self.status
|
|
137
|
+
if self.due:
|
|
138
|
+
# Convert date to datetime for API compatibility
|
|
139
|
+
due_datetime = datetime.combine(self.due, datetime.min.time())
|
|
140
|
+
task_dict['due'] = due_datetime.isoformat() + 'Z'
|
|
141
|
+
if self.completed:
|
|
142
|
+
# Convert date to datetime for API compatibility
|
|
143
|
+
completed_datetime = datetime.combine(self.completed, datetime.min.time())
|
|
144
|
+
task_dict['completed'] = completed_datetime.isoformat() + 'Z'
|
|
145
|
+
if self.parent:
|
|
146
|
+
task_dict['parent'] = self.parent
|
|
147
|
+
if self.position:
|
|
148
|
+
task_dict['position'] = self.position
|
|
149
|
+
return task_dict
|
|
150
|
+
|
|
151
|
+
def __repr__(self):
|
|
152
|
+
due_str = self.due.strftime("%a %m-%d-%Y") if self.due else None
|
|
153
|
+
return (
|
|
154
|
+
f"Task(id={self.task_id!r}, title={self.title!r}, "
|
|
155
|
+
f"status={self.status!r}, due={due_str})"
|
|
156
|
+
)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime, date, time
|
|
3
|
+
from typing import Optional, Dict, Any
|
|
4
|
+
|
|
5
|
+
from .types import Task, TaskList
|
|
6
|
+
from .constants import (
|
|
7
|
+
MAX_TITLE_LENGTH, MAX_NOTES_LENGTH, VALID_TASK_STATUSES,
|
|
8
|
+
RFC3339_FORMAT
|
|
9
|
+
)
|
|
10
|
+
from ...utils.datetime import convert_datetime_to_local_timezone
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Import from shared utilities
|
|
14
|
+
from ...utils.validation import validate_text_field, sanitize_header_value
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def validate_task_status(status: Optional[str]) -> None:
|
|
18
|
+
"""Validates task status."""
|
|
19
|
+
if status and status not in VALID_TASK_STATUSES:
|
|
20
|
+
raise ValueError(f"Invalid task status: {status}. Must be one of: {', '.join(VALID_TASK_STATUSES)}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_datetime_field(field_value: Optional[str]) -> Optional[date]:
|
|
24
|
+
"""
|
|
25
|
+
Parse datetime field from Google Tasks API response to date.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
field_value: ISO datetime string from API
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Parsed date object or None if parsing fails
|
|
32
|
+
"""
|
|
33
|
+
if not field_value:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
# Handle different formats from Tasks API
|
|
38
|
+
if field_value.endswith('Z'):
|
|
39
|
+
dt = datetime.fromisoformat(field_value.replace('Z', '+00:00'))
|
|
40
|
+
else:
|
|
41
|
+
dt = datetime.fromisoformat(field_value)
|
|
42
|
+
|
|
43
|
+
# Convert to local timezone and return date
|
|
44
|
+
dt = convert_datetime_to_local_timezone(dt)
|
|
45
|
+
return dt.date()
|
|
46
|
+
except (ValueError, TypeError):
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_update_datetime_field(field_value: Optional[str]) -> Optional[datetime]:
|
|
51
|
+
"""
|
|
52
|
+
Parse update datetime field from Google Tasks API response.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
field_value: ISO datetime string from API
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Parsed datetime object or None if parsing fails
|
|
59
|
+
"""
|
|
60
|
+
if not field_value:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
# Handle different formats from Tasks API
|
|
65
|
+
if field_value.endswith('Z'):
|
|
66
|
+
dt = datetime.fromisoformat(field_value.replace('Z', '+00:00'))
|
|
67
|
+
else:
|
|
68
|
+
dt = datetime.fromisoformat(field_value)
|
|
69
|
+
|
|
70
|
+
# Convert to local timezone
|
|
71
|
+
return convert_datetime_to_local_timezone(dt)
|
|
72
|
+
except (ValueError, TypeError):
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def from_google_task(google_task: Dict[str, Any], task_list_id: Optional[str] = None) -> Task:
|
|
77
|
+
"""
|
|
78
|
+
Create a Task instance from a Google Tasks API response.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
google_task: Dictionary containing task data from Google Tasks API
|
|
82
|
+
task_list_id: The ID of the task list this task belongs to
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Task instance populated with the data from the dictionary
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
task_id = google_task.get('id')
|
|
89
|
+
title = google_task.get('title', '').strip() if google_task.get('title') else None
|
|
90
|
+
notes = google_task.get('notes', '').strip() if google_task.get('notes') else None
|
|
91
|
+
status = google_task.get('status', 'needsAction')
|
|
92
|
+
|
|
93
|
+
# Validate status
|
|
94
|
+
if status not in VALID_TASK_STATUSES:
|
|
95
|
+
status = 'needsAction'
|
|
96
|
+
|
|
97
|
+
# Parse dates
|
|
98
|
+
due = parse_datetime_field(google_task.get('due'))
|
|
99
|
+
completed = parse_datetime_field(google_task.get('completed'))
|
|
100
|
+
updated = parse_datetime_field(google_task.get('updated'))
|
|
101
|
+
|
|
102
|
+
# Parse hierarchy
|
|
103
|
+
parent = google_task.get('parent')
|
|
104
|
+
position = google_task.get('position')
|
|
105
|
+
|
|
106
|
+
return Task(
|
|
107
|
+
task_id=task_id,
|
|
108
|
+
title=title,
|
|
109
|
+
notes=notes,
|
|
110
|
+
status=status,
|
|
111
|
+
due=due,
|
|
112
|
+
completed=completed,
|
|
113
|
+
updated=updated,
|
|
114
|
+
parent=parent,
|
|
115
|
+
position=position,
|
|
116
|
+
task_list_id=task_list_id
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
raise ValueError("Invalid task data - failed to parse Google task")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def from_google_task_list(google_task_list: Dict[str, Any]) -> TaskList:
|
|
124
|
+
"""
|
|
125
|
+
Create a TaskList instance from a Google Tasks API response.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
google_task_list: Dictionary containing task list data from Google Tasks API
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
TaskList instance populated with the data from the dictionary
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
task_list_id = google_task_list.get('id')
|
|
135
|
+
title = google_task_list.get('title', '').strip() if google_task_list.get('title') else None
|
|
136
|
+
updated = parse_update_datetime_field(google_task_list.get('updated'))
|
|
137
|
+
|
|
138
|
+
return TaskList(
|
|
139
|
+
task_list_id=task_list_id,
|
|
140
|
+
title=title,
|
|
141
|
+
updated=updated
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
raise ValueError("Invalid task list data - failed to parse Google task list")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def create_task_body(
|
|
149
|
+
title: str,
|
|
150
|
+
notes: Optional[str] = None,
|
|
151
|
+
due: Optional[date] = None,
|
|
152
|
+
parent: Optional[str] = None,
|
|
153
|
+
position: Optional[str] = None,
|
|
154
|
+
status: Optional[str] = None
|
|
155
|
+
) -> Dict[str, Any]:
|
|
156
|
+
"""
|
|
157
|
+
Create task body dictionary for Google Tasks API.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
title: Task title
|
|
161
|
+
notes: Task notes
|
|
162
|
+
due: Due date
|
|
163
|
+
parent: Parent task ID
|
|
164
|
+
position: Position in task list
|
|
165
|
+
status: Task status
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Dictionary suitable for Tasks API requests
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
ValueError: If required fields are invalid
|
|
172
|
+
"""
|
|
173
|
+
if not title or not title.strip():
|
|
174
|
+
raise ValueError("Task title cannot be empty")
|
|
175
|
+
|
|
176
|
+
# Validate text fields
|
|
177
|
+
validate_text_field(title, MAX_TITLE_LENGTH, "title")
|
|
178
|
+
validate_text_field(notes, MAX_NOTES_LENGTH, "notes")
|
|
179
|
+
validate_task_status(status)
|
|
180
|
+
|
|
181
|
+
# Build task body
|
|
182
|
+
task_body = {
|
|
183
|
+
'title': sanitize_header_value(title)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Add optional fields
|
|
187
|
+
if notes:
|
|
188
|
+
task_body['notes'] = sanitize_header_value(notes)
|
|
189
|
+
if due:
|
|
190
|
+
# Convert date to datetime for API compatibility
|
|
191
|
+
due_datetime = datetime.combine(due, time.min)
|
|
192
|
+
task_body['due'] = due_datetime.isoformat() + 'Z'
|
|
193
|
+
if parent:
|
|
194
|
+
task_body['parent'] = parent
|
|
195
|
+
if position:
|
|
196
|
+
task_body['position'] = position
|
|
197
|
+
if status:
|
|
198
|
+
task_body['status'] = status
|
|
199
|
+
|
|
200
|
+
return task_body
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def create_task_list_body(title: str) -> Dict[str, Any]:
|
|
204
|
+
"""
|
|
205
|
+
Create task list body dictionary for Google Tasks API.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
title: Task list title
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Dictionary suitable for Tasks API requests
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
ValueError: If required fields are invalid
|
|
215
|
+
"""
|
|
216
|
+
if not title or not title.strip():
|
|
217
|
+
raise ValueError("Task list title cannot be empty")
|
|
218
|
+
|
|
219
|
+
# Validate title length
|
|
220
|
+
validate_text_field(title, MAX_TITLE_LENGTH, "title")
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
'title': sanitize_header_value(title)
|
|
224
|
+
}
|