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.
Files changed (39) hide show
  1. google_api_client_wrapper-1.0.0.dist-info/METADATA +103 -0
  2. google_api_client_wrapper-1.0.0.dist-info/RECORD +39 -0
  3. google_api_client_wrapper-1.0.0.dist-info/WHEEL +5 -0
  4. google_api_client_wrapper-1.0.0.dist-info/licenses/LICENSE +21 -0
  5. google_api_client_wrapper-1.0.0.dist-info/top_level.txt +1 -0
  6. google_client/__init__.py +6 -0
  7. google_client/services/__init__.py +13 -0
  8. google_client/services/calendar/__init__.py +14 -0
  9. google_client/services/calendar/api_service.py +454 -0
  10. google_client/services/calendar/constants.py +48 -0
  11. google_client/services/calendar/exceptions.py +35 -0
  12. google_client/services/calendar/query_builder.py +314 -0
  13. google_client/services/calendar/types.py +403 -0
  14. google_client/services/calendar/utils.py +338 -0
  15. google_client/services/drive/__init__.py +13 -0
  16. google_client/services/drive/api_service.py +1133 -0
  17. google_client/services/drive/constants.py +37 -0
  18. google_client/services/drive/exceptions.py +60 -0
  19. google_client/services/drive/query_builder.py +385 -0
  20. google_client/services/drive/types.py +242 -0
  21. google_client/services/drive/utils.py +392 -0
  22. google_client/services/gmail/__init__.py +16 -0
  23. google_client/services/gmail/api_service.py +715 -0
  24. google_client/services/gmail/constants.py +6 -0
  25. google_client/services/gmail/exceptions.py +45 -0
  26. google_client/services/gmail/query_builder.py +408 -0
  27. google_client/services/gmail/types.py +285 -0
  28. google_client/services/gmail/utils.py +426 -0
  29. google_client/services/tasks/__init__.py +12 -0
  30. google_client/services/tasks/api_service.py +561 -0
  31. google_client/services/tasks/constants.py +32 -0
  32. google_client/services/tasks/exceptions.py +35 -0
  33. google_client/services/tasks/query_builder.py +324 -0
  34. google_client/services/tasks/types.py +156 -0
  35. google_client/services/tasks/utils.py +224 -0
  36. google_client/user_client.py +208 -0
  37. google_client/utils/__init__.py +0 -0
  38. google_client/utils/datetime.py +144 -0
  39. 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
+ }