pytaskwarrior 2.0.4__tar.gz → 2.0.5__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.
Files changed (35) hide show
  1. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/PKG-INFO +1 -1
  2. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/README.md +11 -8
  3. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/pyproject.toml +1 -1
  4. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/pytaskwarrior.egg-info/PKG-INFO +1 -1
  5. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/pytaskwarrior.egg-info/SOURCES.txt +1 -0
  6. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/__init__.py +2 -0
  7. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/adapters/taskwarrior_adapter.py +40 -34
  8. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/dto/__init__.py +2 -1
  9. pytaskwarrior-2.0.5/src/taskwarrior/dto/task_id.py +102 -0
  10. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/dto/uda_dto.py +18 -1
  11. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/main.py +33 -31
  12. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/LICENSE +0 -0
  13. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/PYPI_README.md +0 -0
  14. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/setup.cfg +0 -0
  15. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/__init__.py +0 -0
  16. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/pytaskwarrior.egg-info/dependency_links.txt +0 -0
  17. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/pytaskwarrior.egg-info/requires.txt +0 -0
  18. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/pytaskwarrior.egg-info/top_level.txt +0 -0
  19. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/adapters/__init__.py +0 -0
  20. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/config/config_store.py +0 -0
  21. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/config/uda_parser.py +0 -0
  22. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/dto/annotation_dto.py +0 -0
  23. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/dto/context_dto.py +0 -0
  24. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/dto/task_dto.py +0 -0
  25. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/enums.py +0 -0
  26. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/exceptions.py +0 -0
  27. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/py.typed +0 -0
  28. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/registry/__init__.py +0 -0
  29. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/registry/uda_registry.py +0 -0
  30. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/services/__init__.py +0 -0
  31. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/services/context_service.py +0 -0
  32. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/services/uda_service.py +0 -0
  33. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/utils/__init__.py +0 -0
  34. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/utils/conversions.py +0 -0
  35. {pytaskwarrior-2.0.4 → pytaskwarrior-2.0.5}/src/taskwarrior/utils/dto_converter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytaskwarrior
3
- Version: 2.0.4
3
+ Version: 2.0.5
4
4
  Summary: Taskwarrior wrapper python module
5
5
  Author-email: sznicolas <sznicolas@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -119,15 +119,18 @@ tw = TaskWarrior(
119
119
  | Method | Description |
120
120
  |--------|-------------|
121
121
  | `add_task(task: TaskInputDTO)` | Create a new task |
122
- | `get_task(uuid)` | Get a single task by UUID or ID |
122
+ | `get_task(task_ref)` | Get a single task by UUID, ID, or TaskID |
123
123
  | `get_tasks(filter="", include_completed=False, include_deleted=False)` | Get tasks matching filter |
124
- | `modify_task(task: TaskInputDTO, uuid)` | Modify an existing task |
125
- | `delete_task(uuid)` | Mark task as deleted |
126
- | `purge_task(uuid)` | Permanently remove task |
127
- | `done_task(uuid)` | Mark task as completed |
128
- | `start_task(uuid)` | Start working on task |
129
- | `stop_task(uuid)` | Stop working on task |
130
- | `annotate_task(uuid, annotation)` | Add annotation to task |
124
+ | `modify_task(task: TaskInputDTO, task_ref)` | Modify an existing task |
125
+ | `delete_task(task_ref)` | Mark task as deleted |
126
+ | `purge_task(task_ref)` | Permanently remove task |
127
+ | `done_task(task_ref)` | Mark task as completed |
128
+ | `start_task(task_ref)` | Start working on task |
129
+ | `stop_task(task_ref)` | Stop working on task |
130
+ | `annotate_task(task_ref, annotation)` | Add annotation to task |
131
+
132
+
133
+ Note: "task_ref" accepts an integer working-set index, a UUID string/object, or a `TaskID` instance for clarity. `TaskID` is exported from the top-level package and can be used to create explicit references (e.g., `TaskID(1)`, `TaskID(uuid_obj)`).
131
134
 
132
135
  #### Tags Operations
133
136
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pytaskwarrior"
3
- version = "2.0.4"
3
+ version = "2.0.5"
4
4
  description = "Taskwarrior wrapper python module"
5
5
  readme = "PYPI_README.md"
6
6
  requires-python = ">=3.12"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytaskwarrior
3
- Version: 2.0.4
3
+ Version: 2.0.5
4
4
  Summary: Taskwarrior wrapper python module
5
5
  Author-email: sznicolas <sznicolas@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -21,6 +21,7 @@ src/taskwarrior/dto/__init__.py
21
21
  src/taskwarrior/dto/annotation_dto.py
22
22
  src/taskwarrior/dto/context_dto.py
23
23
  src/taskwarrior/dto/task_dto.py
24
+ src/taskwarrior/dto/task_id.py
24
25
  src/taskwarrior/dto/uda_dto.py
25
26
  src/taskwarrior/registry/__init__.py
26
27
  src/taskwarrior/registry/uda_registry.py
@@ -37,6 +37,7 @@ import importlib.metadata
37
37
  from .dto.annotation_dto import AnnotationDTO
38
38
  from .dto.context_dto import ContextDTO
39
39
  from .dto.task_dto import TaskInputDTO, TaskOutputDTO
40
+ from .dto.task_id import TaskID
40
41
  from .dto.uda_dto import UdaConfig, UdaType
41
42
  from .enums import Priority, RecurrencePeriod, TaskStatus
42
43
  from .exceptions import (
@@ -61,6 +62,7 @@ __all__ = [
61
62
  "version",
62
63
  "Priority",
63
64
  "RecurrencePeriod",
65
+ "TaskID",
64
66
  "TaskStatus",
65
67
  "TaskConfigurationError",
66
68
  "TaskInputDTO",
@@ -14,6 +14,7 @@ from uuid import UUID
14
14
 
15
15
  from ..config.config_store import ConfigStore
16
16
  from ..dto.task_dto import TaskInputDTO, TaskOutputDTO
17
+ from ..dto.task_id import TaskID, TaskRef
17
18
  from ..enums import TaskStatus
18
19
  from ..exceptions import (
19
20
  TaskConfigurationError,
@@ -26,6 +27,10 @@ from ..exceptions import (
26
27
 
27
28
  logger = logging.getLogger(__name__)
28
29
 
30
+ def _to_taskid(value: TaskRef) -> TaskID:
31
+ """Normalize a TaskRef into a TaskID instance."""
32
+ return value if isinstance(value, TaskID) else TaskID(value)
33
+
29
34
  TASKWARRIOR_VIRTUAL_TAGS: tuple[str, ...] = (
30
35
  "BLOCKED",
31
36
  "UNBLOCKED",
@@ -258,28 +263,29 @@ class TaskWarriorAdapter:
258
263
  logger.info(f"Successfully added task with UUID: {added_task.uuid}")
259
264
  return added_task
260
265
 
261
- def modify_task(self, task: TaskInputDTO, task_id_or_uuid: str | int | UUID) -> TaskOutputDTO:
266
+ def modify_task(self, task: TaskInputDTO, task_id: str | int | UUID | TaskID) -> TaskOutputDTO:
262
267
  """Modify an existing task. Returns the updated task."""
263
- logger.info(f"Modifying task with UUID: {task_id_or_uuid}")
268
+ logger.info(f"Modifying task with UUID: {task_id}")
269
+ tid = _to_taskid(task_id)
264
270
 
265
271
  args = self._build_args(task)
266
- result = self.run_task_command([str(task_id_or_uuid), "modify"] + args)
272
+ result = self.run_task_command([str(tid), "modify"] + args)
267
273
 
268
274
  if result.returncode != 0:
269
275
  error_msg = f"Failed to modify task: {result.stderr}"
270
276
  logger.error(error_msg)
271
277
  raise TaskWarriorError(error_msg)
272
278
 
273
- updated_task = self.get_task(task_id_or_uuid)
274
- logger.info(f"Successfully modified task with UUID: {task_id_or_uuid}")
279
+ updated_task = self.get_task(tid)
280
+ logger.info(f"Successfully modified task with UUID: {tid}")
275
281
  return updated_task
276
282
 
277
- def get_task(self, task_id_or_uuid: str | int | UUID, filter_args: str = "") -> TaskOutputDTO:
283
+ def get_task(self, task_id: str | int | UUID | TaskID, filter_args: str = "") -> TaskOutputDTO:
278
284
  """Retrieve a single task by ID or UUID."""
279
- task_id_or_uuid = str(task_id_or_uuid)
280
- logger.debug(f"Retrieving task with ID/UUID: {task_id_or_uuid}")
285
+ tid = _to_taskid(task_id)
286
+ logger.debug(f"Retrieving task with ID/UUID: {tid}")
281
287
 
282
- args = [filter_args, task_id_or_uuid, "export"]
288
+ args = [filter_args, str(tid), "export"]
283
289
  result = self.run_task_command(args)
284
290
  if result.returncode == 0:
285
291
  try:
@@ -290,17 +296,17 @@ class TaskWarriorAdapter:
290
296
  return task
291
297
  elif len(tasks_data) == 0:
292
298
  raise TaskNotFound(
293
- f"No task ID/UUID {task_id_or_uuid} with filter {filter_args}"
299
+ f"No task ID/UUID {tid} with filter {filter_args}"
294
300
  )
295
301
  else:
296
302
  raise TaskWarriorError(
297
- f"More than one task returned for ID/UUID {task_id_or_uuid} with filter '{filter_args}'"
303
+ f"More than one task returned for ID/UUID {tid} with filter '{filter_args}'"
298
304
  )
299
305
  except json.JSONDecodeError as e:
300
306
  logger.error(f"Failed to parse JSON response: {e}")
301
307
  raise TaskWarriorError(f"Invalid response from TaskWarrior: {result.stdout}") from e
302
308
  else:
303
- raise TaskNotFound(f"Task ID/UUID {task_id_or_uuid} not found")
309
+ raise TaskNotFound(f"Task ID/UUID {tid} not found")
304
310
 
305
311
  def get_tasks(
306
312
  self,
@@ -364,13 +370,13 @@ class TaskWarriorAdapter:
364
370
  logger.error(f"Failed to parse JSON response: {e}")
365
371
  raise TaskWarriorError(f"Invalid response from TaskWarrior: {result.stdout}") from e
366
372
 
367
- def get_recurring_task(self, task_id_or_uuid: str | int | UUID) -> TaskOutputDTO:
373
+ def get_recurring_task(self, task_id: str | int | UUID | TaskID) -> TaskOutputDTO:
368
374
  """Get the parent recurring task template."""
369
- task_id_or_uuid = str(task_id_or_uuid)
370
- logger.debug(f"Getting recurring task with UUID: {task_id_or_uuid}")
375
+ tid = _to_taskid(task_id)
376
+ logger.debug(f"Getting recurring task with UUID: {tid}")
371
377
 
372
378
  result = self.run_task_command(
373
- [str(task_id_or_uuid), "status:" + TaskStatus.RECURRING, "export"]
379
+ [str(tid), "status:" + TaskStatus.RECURRING, "export"]
374
380
  )
375
381
 
376
382
  if result.returncode == 0:
@@ -385,16 +391,16 @@ class TaskWarriorAdapter:
385
391
  return task
386
392
 
387
393
  logger.debug(
388
- f"Recurring task {task_id_or_uuid} not found as recurring, trying normal retrieval"
394
+ f"Recurring task {tid} not found as recurring, trying normal retrieval"
389
395
  )
390
- return self.get_task(task_id_or_uuid)
396
+ return self.get_task(tid)
391
397
 
392
- def get_recurring_instances(self, task_id_or_uuid: str | int | UUID) -> list[TaskOutputDTO]:
398
+ def get_recurring_instances(self, task_id: str | int | UUID | TaskID) -> list[TaskOutputDTO]:
393
399
  """Get all instances of a recurring task."""
394
- task_id_or_uuid = str(task_id_or_uuid)
395
- logger.debug(f"Getting recurring instances for parent UUID: {task_id_or_uuid}")
400
+ tid = _to_taskid(task_id)
401
+ logger.debug(f"Getting recurring instances for parent UUID: {tid}")
396
402
 
397
- result = self.run_task_command([f"parent:{str(task_id_or_uuid)}", "export"])
403
+ result = self.run_task_command([f"parent:{str(tid)}", "export"])
398
404
 
399
405
  if result.returncode != 0:
400
406
  if (
@@ -420,9 +426,9 @@ class TaskWarriorAdapter:
420
426
  logger.error(f"Failed to parse JSON response: {e}")
421
427
  raise TaskWarriorError(f"Invalid response from TaskWarrior: {result.stdout}") from e
422
428
 
423
- def delete_task(self, task_id_or_uuid: str | int | UUID) -> None:
429
+ def delete_task(self, task_id: str | int | UUID | TaskID) -> None:
424
430
  """Mark a task as deleted."""
425
- task_ref = str(task_id_or_uuid)
431
+ task_ref = str(_to_taskid(task_id))
426
432
  logger.info(f"Deleting task: {task_ref}")
427
433
 
428
434
  result = self.run_task_command([task_ref, "delete"])
@@ -434,9 +440,9 @@ class TaskWarriorAdapter:
434
440
 
435
441
  logger.info(f"Successfully deleted task: {task_ref}")
436
442
 
437
- def purge_task(self, task_id_or_uuid: str | int | UUID) -> None:
443
+ def purge_task(self, task_id: str | int | UUID | TaskID) -> None:
438
444
  """Permanently remove a task."""
439
- task_ref = str(task_id_or_uuid)
445
+ task_ref = str(_to_taskid(task_id))
440
446
  logger.info(f"Purging task: {task_ref}")
441
447
 
442
448
  result = self.run_task_command([task_ref, "purge"])
@@ -448,9 +454,9 @@ class TaskWarriorAdapter:
448
454
 
449
455
  logger.info(f"Successfully purged task: {task_ref}")
450
456
 
451
- def done_task(self, task_id_or_uuid: str | int | UUID) -> None:
457
+ def done_task(self, task_id: str | int | UUID | TaskID) -> None:
452
458
  """Mark a task as completed."""
453
- task_ref = str(task_id_or_uuid)
459
+ task_ref = str(_to_taskid(task_id))
454
460
  logger.info(f"Completing task: {task_ref}")
455
461
 
456
462
  result = self.run_task_command([task_ref, "done"])
@@ -462,9 +468,9 @@ class TaskWarriorAdapter:
462
468
 
463
469
  logger.info(f"Successfully completed task: {task_ref}")
464
470
 
465
- def start_task(self, task_id_or_uuid: str | int | UUID) -> None:
471
+ def start_task(self, task_id: str | int | UUID | TaskID) -> None:
466
472
  """Start working on a task."""
467
- task_ref = str(task_id_or_uuid)
473
+ task_ref = str(_to_taskid(task_id))
468
474
  logger.info(f"Starting task: {task_ref}")
469
475
 
470
476
  result = self.run_task_command([task_ref, "start"])
@@ -476,9 +482,9 @@ class TaskWarriorAdapter:
476
482
 
477
483
  logger.info(f"Successfully started task: {task_ref}")
478
484
 
479
- def stop_task(self, task_id_or_uuid: str | int | UUID) -> None:
485
+ def stop_task(self, task_id: str | int | UUID | TaskID) -> None:
480
486
  """Stop working on a task."""
481
- task_ref = str(task_id_or_uuid)
487
+ task_ref = str(_to_taskid(task_id))
482
488
  logger.info(f"Stopping task: {task_ref}")
483
489
 
484
490
  result = self.run_task_command([task_ref, "stop"])
@@ -490,9 +496,9 @@ class TaskWarriorAdapter:
490
496
 
491
497
  logger.info(f"Successfully stopped task: {task_ref}")
492
498
 
493
- def annotate_task(self, task_id_or_uuid: str | int | UUID, annotation: str) -> None:
499
+ def annotate_task(self, task_id: str | int | UUID | TaskID, annotation: str) -> None:
494
500
  """Add an annotation to a task."""
495
- task_ref = str(task_id_or_uuid)
501
+ task_ref = str(_to_taskid(task_id))
496
502
  logger.info(f"Annotating task {task_ref} with: {annotation}")
497
503
 
498
504
  sanitized_annotation = shlex.quote(annotation)
@@ -1,5 +1,6 @@
1
1
  from .annotation_dto import AnnotationDTO
2
2
  from .context_dto import ContextDTO
3
3
  from .task_dto import TaskInputDTO, TaskOutputDTO
4
+ from .task_id import TaskID
4
5
 
5
- __all__ = ["AnnotationDTO", "ContextDTO", "TaskInputDTO", "TaskOutputDTO"]
6
+ __all__ = ["AnnotationDTO", "ContextDTO", "TaskID", "TaskInputDTO", "TaskOutputDTO"]
@@ -0,0 +1,102 @@
1
+ """TaskID — unified identifier for a TaskWarrior task.
2
+
3
+ A task can be referenced either by its working-set index (``int``) or by its
4
+ persistent UUID. This class wraps both forms under a single type so that
5
+ every API method only needs one parameter type instead of ``str | int | UUID``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from uuid import UUID
11
+
12
+ from ..exceptions import TaskValidationError
13
+
14
+
15
+ class TaskID:
16
+ """Unified identifier for a TaskWarrior task.
17
+
18
+ A ``TaskID`` holds either the task's working-set index (a positive integer)
19
+ or its UUID, and produces the correct string representation for the
20
+ TaskWarrior CLI via ``str(task_id)``.
21
+
22
+ TaskWarrior also supports partial UUID prefixes; these are accepted as-is
23
+ when provided as a plain string.
24
+
25
+ Args:
26
+ value: The task identifier — one of:
27
+
28
+ * ``int`` — working-set index (must be > 0).
29
+ * ``UUID`` — the task's persistent UUID.
30
+ * ``str`` — a UUID string, an integer string, or a UUID prefix.
31
+
32
+ Raises:
33
+ TaskValidationError: If *value* is an integer ≤ 0 or an empty string.
34
+
35
+ Example:
36
+ >>> TaskID(1)
37
+ TaskID('1')
38
+ >>> TaskID("abc-def-uuid")
39
+ TaskID('abc-def-uuid')
40
+ >>> TaskID(some_uuid_obj)
41
+ TaskID('550e8400-e29b-41d4-a716-446655440000')
42
+
43
+ Factory from an existing task output::
44
+
45
+ task = tw.get_task(TaskID(1))
46
+ tw.done_task(TaskID.from_task(task))
47
+ """
48
+
49
+ __slots__ = ("_value",)
50
+
51
+ def __init__(self, value: str | int | UUID) -> None:
52
+ if isinstance(value, int):
53
+ if value <= 0:
54
+ raise TaskValidationError(
55
+ f"Task working-set index must be a positive integer, got {value}"
56
+ )
57
+ self._value: str = str(value)
58
+ elif isinstance(value, UUID):
59
+ self._value = str(value)
60
+ elif isinstance(value, str):
61
+ stripped = value.strip()
62
+ if not stripped:
63
+ raise TaskValidationError("TaskID string cannot be empty")
64
+ self._value = stripped
65
+ else:
66
+ raise TaskValidationError(
67
+ f"TaskID requires str, int, or UUID — got {type(value).__name__!r}"
68
+ )
69
+
70
+ @classmethod
71
+ def from_task(cls, task: TaskOutputDTO) -> TaskID: # type: ignore[name-defined] # noqa: F821
72
+ """Create a ``TaskID`` from the UUID of an existing task output.
73
+
74
+ Args:
75
+ task: A :class:`~taskwarrior.dto.task_dto.TaskOutputDTO` instance.
76
+
77
+ Returns:
78
+ A ``TaskID`` wrapping the task's UUID.
79
+
80
+ Example:
81
+ >>> task = tw.get_task(TaskID(1))
82
+ >>> tw.done_task(TaskID.from_task(task))
83
+ """
84
+ return cls(task.uuid)
85
+
86
+ def __str__(self) -> str:
87
+ return self._value
88
+
89
+ def __repr__(self) -> str:
90
+ return f"TaskID({self._value!r})"
91
+
92
+ def __eq__(self, other: object) -> bool:
93
+ if isinstance(other, TaskID):
94
+ return self._value == other._value
95
+ return NotImplemented
96
+
97
+ def __hash__(self) -> int:
98
+ return hash(self._value)
99
+
100
+
101
+ # Type alias to reduce repeated unions across the codebase
102
+ type TaskRef = str | int | UUID | TaskID
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  from enum import Enum
10
10
 
11
- from pydantic import BaseModel, Field
11
+ from pydantic import BaseModel, Field, model_validator
12
12
 
13
13
 
14
14
  class UdaType(str, Enum):
@@ -36,6 +36,8 @@ class UdaType(str, Enum):
36
36
  UUID = "uuid"
37
37
 
38
38
 
39
+
40
+
39
41
  class UdaConfig(BaseModel):
40
42
  """Data Transfer Object for User Defined Attributes (UDAs).
41
43
 
@@ -78,3 +80,18 @@ class UdaConfig(BaseModel):
78
80
  )
79
81
 
80
82
  model_config = {"populate_by_name": True, "extra": "forbid"}
83
+
84
+ @model_validator(mode="before")
85
+ @classmethod
86
+ def alias_type_to_uda_type(cls, values: object) -> object:
87
+ # Accept 'type' as an alias for 'uda_type', unless 'uda_type' is already present
88
+ if isinstance(values, dict):
89
+ # Accept 'type' as an alias for 'uda_type', unless 'uda_type' is already present
90
+ if "type" in values and "uda_type" not in values:
91
+ values = dict(values) # copy to avoid mutating input
92
+ values["uda_type"] = values.pop("type")
93
+ # Remove 'type' if both are present, to avoid extra field error
94
+ elif "type" in values and "uda_type" in values:
95
+ values = dict(values)
96
+ values.pop("type")
97
+ return values
@@ -8,11 +8,11 @@ from __future__ import annotations
8
8
  import logging
9
9
  import os
10
10
  from typing import Any
11
- from uuid import UUID
12
11
 
13
12
  from .adapters.taskwarrior_adapter import TaskWarriorAdapter
14
13
  from .dto.context_dto import ContextDTO
15
14
  from .dto.task_dto import TaskInputDTO, TaskOutputDTO
15
+ from .dto.task_id import TaskRef
16
16
  from .dto.uda_dto import UdaConfig
17
17
  from .enums import TaskStatus # noqa: F401 — re-exported for public API
18
18
  from .services.context_service import ContextService
@@ -114,12 +114,12 @@ class TaskWarrior:
114
114
  """
115
115
  return self.adapter.add_task(task)
116
116
 
117
- def modify_task(self, task: TaskInputDTO, task_id_or_uuid: str | int | UUID) -> TaskOutputDTO:
117
+ def modify_task(self, task: TaskInputDTO, task_id: TaskRef) -> TaskOutputDTO:
118
118
  """Modify an existing task.
119
119
 
120
120
  Args:
121
121
  task: The new task data to apply.
122
- task_id_or_uuid: The task ID (integer) or UUID to modify.
122
+ task_id: The task ID (integer) or UUID to modify.
123
123
 
124
124
  Returns:
125
125
  The updated task.
@@ -132,13 +132,13 @@ class TaskWarrior:
132
132
  >>> task = TaskInputDTO(description="Updated description")
133
133
  >>> updated = tw.modify_task(task, "abc-123-uuid")
134
134
  """
135
- return self.adapter.modify_task(task, task_id_or_uuid)
135
+ return self.adapter.modify_task(task, task_id)
136
136
 
137
- def get_task(self, task_id_or_uuid: str | int | UUID) -> TaskOutputDTO:
137
+ def get_task(self, task_id: TaskRef) -> TaskOutputDTO:
138
138
  """Retrieve a single task by ID or UUID.
139
139
 
140
140
  Args:
141
- task_id_or_uuid: The task ID (integer) or UUID to retrieve.
141
+ task_id: The task ID (integer) or UUID to retrieve.
142
142
 
143
143
  Returns:
144
144
  The requested task.
@@ -149,8 +149,9 @@ class TaskWarrior:
149
149
  Example:
150
150
  >>> task = tw.get_task(1) # By ID
151
151
  >>> task = tw.get_task("abc-123-uuid") # By UUID
152
+ >>> task = tw.get_task(TaskID(1)) # Using TaskID
152
153
  """
153
- return self.adapter.get_task(task_id_or_uuid)
154
+ return self.adapter.get_task(task_id)
154
155
 
155
156
  def get_tasks(
156
157
  self,
@@ -210,11 +211,11 @@ class TaskWarrior:
210
211
  include_deleted=include_deleted,
211
212
  )
212
213
 
213
- def get_recurring_task(self, task_id_or_uuid: str | int | UUID) -> TaskOutputDTO:
214
+ def get_recurring_task(self, task_id: TaskRef) -> TaskOutputDTO:
214
215
  """Get the parent recurring task template.
215
216
 
216
217
  Args:
217
- task_id_or_uuid: The UUID of a recurring task or one of its instances.
218
+ task_id: The UUID of a recurring task or one of its instances.
218
219
 
219
220
  Returns:
220
221
  The parent recurring task template.
@@ -222,13 +223,13 @@ class TaskWarrior:
222
223
  Raises:
223
224
  TaskNotFound: If the task doesn't exist.
224
225
  """
225
- return self.adapter.get_recurring_task(task_id_or_uuid)
226
+ return self.adapter.get_recurring_task(task_id)
226
227
 
227
- def get_recurring_instances(self, task_id_or_uuid: str | int | UUID) -> list[TaskOutputDTO]:
228
+ def get_recurring_instances(self, task_id: TaskRef) -> list[TaskOutputDTO]:
228
229
  """Get all instances of a recurring task.
229
230
 
230
231
  Args:
231
- task_id_or_uuid: The UUID of the parent recurring task.
232
+ task_id: The UUID of the parent recurring task.
232
233
 
233
234
  Returns:
234
235
  List of task instances created from the recurring template.
@@ -236,39 +237,39 @@ class TaskWarrior:
236
237
  Raises:
237
238
  TaskNotFound: If the parent task doesn't exist.
238
239
  """
239
- return self.adapter.get_recurring_instances(task_id_or_uuid)
240
+ return self.adapter.get_recurring_instances(task_id)
240
241
 
241
- def delete_task(self, task_id_or_uuid: str | int | UUID) -> None:
242
+ def delete_task(self, task_id: TaskRef) -> None:
242
243
  """Mark a task as deleted.
243
244
 
244
245
  The task is not permanently removed; use `purge_task` for that.
245
246
 
246
247
  Args:
247
- task_id_or_uuid: The task ID or UUID to delete.
248
+ task_id: The task ID or UUID to delete.
248
249
 
249
250
  Raises:
250
251
  TaskOperationError: If the operation fails (e.g., task already deleted).
251
252
  """
252
- self.adapter.delete_task(task_id_or_uuid)
253
+ self.adapter.delete_task(task_id)
253
254
 
254
- def purge_task(self, task_id_or_uuid: str | int | UUID) -> None:
255
+ def purge_task(self, task_id: TaskRef) -> None:
255
256
  """Permanently remove a task from the database.
256
257
 
257
258
  Unlike `delete_task`, this cannot be undone.
258
259
 
259
260
  Args:
260
- task_id_or_uuid: The task ID or UUID to purge.
261
+ task_id: The task ID or UUID to purge.
261
262
 
262
263
  Raises:
263
264
  TaskOperationError: If the operation fails (e.g., task was not deleted first).
264
265
  """
265
- self.adapter.purge_task(task_id_or_uuid)
266
+ self.adapter.purge_task(task_id)
266
267
 
267
- def done_task(self, task_id_or_uuid: str | int | UUID) -> None:
268
+ def done_task(self, task_id: TaskRef) -> None:
268
269
  """Mark a task as completed.
269
270
 
270
271
  Args:
271
- task_id_or_uuid: The task ID or UUID to complete.
272
+ task_id: The task ID or UUID to complete.
272
273
 
273
274
  Raises:
274
275
  TaskOperationError: If the operation fails (e.g., task is already completed).
@@ -276,42 +277,43 @@ class TaskWarrior:
276
277
  Example:
277
278
  >>> tw.done_task(1)
278
279
  >>> tw.done_task("abc-123-uuid")
280
+ >>> tw.done_task(TaskID(1))
279
281
  """
280
- self.adapter.done_task(task_id_or_uuid)
282
+ self.adapter.done_task(task_id)
281
283
 
282
- def start_task(self, task_id_or_uuid: str | int | UUID) -> None:
284
+ def start_task(self, task_id: TaskRef) -> None:
283
285
  """Start working on a task.
284
286
 
285
287
  Sets the task's start time to now, indicating active work.
286
288
 
287
289
  Args:
288
- task_id_or_uuid: The task ID or UUID to start.
290
+ task_id: The task ID or UUID to start.
289
291
 
290
292
  Raises:
291
293
  TaskOperationError: If the operation fails (e.g., task is already started).
292
294
  """
293
- self.adapter.start_task(task_id_or_uuid)
295
+ self.adapter.start_task(task_id)
294
296
 
295
- def stop_task(self, task_id_or_uuid: str | int | UUID) -> None:
297
+ def stop_task(self, task_id: TaskRef) -> None:
296
298
  """Stop working on a task.
297
299
 
298
300
  Clears the task's start time.
299
301
 
300
302
  Args:
301
- task_id_or_uuid: The task ID or UUID to stop.
303
+ task_id: The task ID or UUID to stop.
302
304
 
303
305
  Raises:
304
306
  TaskOperationError: If the operation fails (e.g., task was not started).
305
307
  """
306
- self.adapter.stop_task(task_id_or_uuid)
308
+ self.adapter.stop_task(task_id)
307
309
 
308
- def annotate_task(self, task_id_or_uuid: str | int | UUID, annotation: str) -> None:
310
+ def annotate_task(self, task_id: TaskRef, annotation: str) -> None:
309
311
  """Add an annotation (note) to a task.
310
312
 
311
313
  Annotations are timestamped notes attached to tasks.
312
314
 
313
315
  Args:
314
- task_id_or_uuid: The task ID or UUID to annotate.
316
+ task_id: The task ID or UUID to annotate.
315
317
  annotation: The annotation text to add.
316
318
 
317
319
  Raises:
@@ -320,7 +322,7 @@ class TaskWarrior:
320
322
  Example:
321
323
  >>> tw.annotate_task(1, "Discussed with team, need more info")
322
324
  """
323
- self.adapter.annotate_task(task_id_or_uuid, annotation)
325
+ self.adapter.annotate_task(task_id, annotation)
324
326
 
325
327
  def define_context(self, context: ContextDTO) -> None:
326
328
  """Define a new context from a ContextDTO.
File without changes
File without changes