Skip to content

Commit e4e6d79

Browse files
committed
Pass date and datetime arguments as objects
1 parent 222e621 commit e4e6d79

File tree

3 files changed

+99
-74
lines changed

3 files changed

+99
-74
lines changed

tests/test_api_tasks.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from datetime import UTC, datetime
34
from typing import TYPE_CHECKING, Any
45

56
import pytest
@@ -244,6 +245,7 @@ async def test_add_task_full(
244245
default_task: Task,
245246
) -> None:
246247
content = "Some content"
248+
due_datetime = datetime(2021, 1, 1, 11, 0, 0, tzinfo=UTC)
247249
args: dict[str, Any] = {
248250
"description": "A description",
249251
"project_id": "123",
@@ -252,8 +254,6 @@ async def test_add_task_full(
252254
"labels": ["label1", "label2"],
253255
"priority": 4,
254256
"due_string": "today",
255-
"due_date": "2021-01-01",
256-
"due_datetime": "2021-01-01T11:00:00Z",
257257
"due_lang": "en",
258258
"assignee_id": "321",
259259
"order": 3,
@@ -268,15 +268,26 @@ async def test_add_task_full(
268268
url=f"{DEFAULT_API_URL}/tasks",
269269
json=default_task_response,
270270
status=200,
271-
match=[auth_matcher(), data_matcher({"content": content} | args)],
271+
match=[
272+
auth_matcher(),
273+
data_matcher(
274+
{
275+
"content": content,
276+
"due_datetime": due_datetime.strftime("%Y-%m-%dT%H:%M:%SZ"),
277+
}
278+
| args
279+
),
280+
],
272281
)
273282

274-
new_task = todoist_api.add_task(content=content, **args)
283+
new_task = todoist_api.add_task(content=content, due_datetime=due_datetime, **args)
275284

276285
assert len(requests_mock.calls) == 1
277286
assert new_task == default_task
278287

279-
new_task = await todoist_api_async.add_task(content=content, **args)
288+
new_task = await todoist_api_async.add_task(
289+
content=content, due_datetime=due_datetime, **args
290+
)
280291

281292
assert len(requests_mock.calls) == 2
282293
assert new_task == default_task

todoist_api_python/api.py

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from collections.abc import Callable, Iterator
4+
from datetime import UTC
45
from typing import TYPE_CHECKING, Annotated, Any, Literal, Self, TypeVar
56
from weakref import finalize
67

@@ -33,12 +34,10 @@
3334
)
3435

3536
if TYPE_CHECKING:
37+
from datetime import date, datetime
3638
from types import TracebackType
3739

38-
DateFormat = Annotated[str, Predicate(lambda x: len(x) == 10 and x.count("-") == 2)] # noqa: PLR2004
39-
DateTimeFormat = Annotated[
40-
str, Predicate(lambda x: len(x) >= 20 and "T" in x and ":" in x) # noqa: PLR2004
41-
]
40+
4241
LanguageCode = Annotated[str, Predicate(lambda x: len(x) == 2)] # noqa: PLR2004
4342
ColorString = Annotated[
4443
str,
@@ -230,16 +229,16 @@ def add_task( # noqa: PLR0912
230229
labels: list[Annotated[str, MaxLen(100)]] | None = None,
231230
priority: Annotated[int, Ge(1), Le(4)] | None = None,
232231
due_string: Annotated[str, MaxLen(150)] | None = None,
233-
due_date: DateFormat | None = None,
234-
due_datetime: DateTimeFormat | None = None,
235232
due_lang: LanguageCode | None = None,
233+
due_date: date | None = None,
234+
due_datetime: datetime | None = None,
236235
assignee_id: str | None = None,
237236
order: int | None = None,
238237
auto_reminder: bool | None = None,
239238
auto_parse_labels: bool | None = None,
240239
duration: Annotated[int, Ge(1)] | None = None,
241240
duration_unit: Literal["minute", "day"] | None = None,
242-
deadline_date: DateFormat | None = None,
241+
deadline_date: date | None = None,
243242
deadline_lang: LanguageCode | None = None,
244243
) -> Task:
245244
"""
@@ -252,17 +251,17 @@ def add_task( # noqa: PLR0912
252251
:param labels: The task's labels (a list of names).
253252
:param priority: The priority of the task (4 for very urgent).
254253
:param due_string: The due date in natural language format.
255-
:param due_date: The due date in YYYY-MM-DD format.
256-
:param due_datetime: The due date and time in RFC 3339 format.
257254
:param due_lang: Language for parsing the due date (e.g., 'en').
255+
:param due_date: The due date as a date object.
256+
:param due_datetime: The due date and time as a datetime object.
258257
:param assignee_id: User ID to whom the task is assigned.
259258
:param description: Description for the task.
260259
:param order: The order of task in the project or section.
261260
:param auto_reminder: Whether to add default reminder if date with time is set.
262261
:param auto_parse_labels: Whether to parse labels from task content.
263262
:param duration: The amount of time the task will take.
264263
:param duration_unit: The unit of time for duration.
265-
:param deadline_date: The deadline date in YYYY-MM-DD format.
264+
:param deadline_date: The deadline date as a date object.
266265
:param deadline_lang: Language for parsing the deadline date.
267266
:return: The newly created task.
268267
:raises requests.exceptions.HTTPError: If the API request fails.
@@ -285,12 +284,12 @@ def add_task( # noqa: PLR0912
285284
data["priority"] = priority
286285
if due_string is not None:
287286
data["due_string"] = due_string
288-
if due_date is not None:
289-
data["due_date"] = due_date
290-
if due_datetime is not None:
291-
data["due_datetime"] = due_datetime
292287
if due_lang is not None:
293288
data["due_lang"] = due_lang
289+
if due_date is not None:
290+
data["due_date"] = _format_date(due_date)
291+
if due_datetime is not None:
292+
data["due_datetime"] = _format_datetime(due_datetime)
294293
if assignee_id is not None:
295294
data["assignee_id"] = assignee_id
296295
if order is not None:
@@ -304,7 +303,7 @@ def add_task( # noqa: PLR0912
304303
if duration_unit is not None:
305304
data["duration_unit"] = duration_unit
306305
if deadline_date is not None:
307-
data["deadline_date"] = deadline_date
306+
data["deadline_date"] = _format_date(deadline_date)
308307
if deadline_lang is not None:
309308
data["deadline_lang"] = deadline_lang
310309

@@ -362,15 +361,15 @@ def update_task( # noqa: PLR0912
362361
labels: list[Annotated[str, MaxLen(60)]] | None = None,
363362
priority: Annotated[int, Ge(1), Le(4)] | None = None,
364363
due_string: Annotated[str, MaxLen(150)] | None = None,
365-
due_date: DateFormat | None = None,
366-
due_datetime: DateTimeFormat | None = None,
367364
due_lang: LanguageCode | None = None,
365+
due_date: date | None = None,
366+
due_datetime: datetime | None = None,
368367
assignee_id: str | None = None,
369368
day_order: int | None = None,
370369
collapsed: bool | None = None,
371370
duration: Annotated[int, Ge(1)] | None = None,
372371
duration_unit: Literal["minute", "day"] | None = None,
373-
deadline_date: DateFormat | None = None,
372+
deadline_date: date | None = None,
374373
deadline_lang: LanguageCode | None = None,
375374
) -> Task:
376375
"""
@@ -384,15 +383,15 @@ def update_task( # noqa: PLR0912
384383
:param labels: The task's labels (a list of names).
385384
:param priority: The priority of the task (4 for very urgent).
386385
:param due_string: The due date in natural language format.
387-
:param due_date: The due date in YYYY-MM-DD format.
388-
:param due_datetime: The due date and time in RFC 3339 format.
389386
:param due_lang: Language for parsing the due date (e.g., 'en').
387+
:param due_date: The due date as a date object.
388+
:param due_datetime: The due date and time as a datetime object.
390389
:param assignee_id: User ID to whom the task is assigned.
391390
:param day_order: The order of the task inside Today or Next 7 days view.
392391
:param collapsed: Whether the task's sub-tasks are collapsed.
393392
:param duration: The amount of time the task will take.
394393
:param duration_unit: The unit of time for duration.
395-
:param deadline_date: The deadline date in YYYY-MM-DD format.
394+
:param deadline_date: The deadline date as a date object.
396395
:param deadline_lang: Language for parsing the deadline date.
397396
:return: the updated Task.
398397
:raises requests.exceptions.HTTPError: If the API request fails.
@@ -410,12 +409,12 @@ def update_task( # noqa: PLR0912
410409
data["priority"] = priority
411410
if due_string is not None:
412411
data["due_string"] = due_string
413-
if due_date is not None:
414-
data["due_date"] = due_date
415-
if due_datetime is not None:
416-
data["due_datetime"] = due_datetime
417412
if due_lang is not None:
418413
data["due_lang"] = due_lang
414+
if due_date is not None:
415+
data["due_date"] = _format_date(due_date)
416+
if due_datetime is not None:
417+
data["due_datetime"] = _format_datetime(due_datetime)
419418
if assignee_id is not None:
420419
data["assignee_id"] = assignee_id
421420
if day_order is not None:
@@ -427,7 +426,7 @@ def update_task( # noqa: PLR0912
427426
if duration_unit is not None:
428427
data["duration_unit"] = duration_unit
429428
if deadline_date is not None:
430-
data["deadline_date"] = deadline_date
429+
data["deadline_date"] = _format_date(deadline_date)
431430
if deadline_lang is not None:
432431
data["deadline_lang"] = deadline_lang
433432

@@ -1152,3 +1151,19 @@ def __next__(self) -> list[T]:
11521151

11531152
results: list[Any] = data.get(self._results_field, [])
11541153
return [self._results_inst(result) for result in results]
1154+
1155+
1156+
def _format_date(d: date) -> str:
1157+
"""Format a date object as YYYY-MM-DD."""
1158+
return d.isoformat()
1159+
1160+
1161+
def _format_datetime(dt: datetime) -> str:
1162+
"""
1163+
Format a datetime object.
1164+
1165+
YYYY-MM-DDTHH:MM:SS for naive datetimes; YYYY-MM-DDTHH:MM:SSZ for aware datetimes.
1166+
"""
1167+
if dt.tzinfo is None:
1168+
return dt.isoformat()
1169+
return dt.astimezone(UTC).isoformat().replace("+00:00", "Z")

0 commit comments

Comments
 (0)