pytaskwarrior 2.0.3__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.
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/PKG-INFO +1 -1
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/README.md +11 -8
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/pyproject.toml +1 -1
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/pytaskwarrior.egg-info/PKG-INFO +1 -1
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/pytaskwarrior.egg-info/SOURCES.txt +1 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/__init__.py +2 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/adapters/taskwarrior_adapter.py +40 -34
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/dto/__init__.py +2 -1
- pytaskwarrior-2.0.5/src/taskwarrior/dto/task_id.py +102 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/dto/uda_dto.py +18 -1
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/main.py +74 -31
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/services/uda_service.py +35 -2
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/LICENSE +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/PYPI_README.md +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/setup.cfg +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/__init__.py +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/pytaskwarrior.egg-info/dependency_links.txt +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/pytaskwarrior.egg-info/requires.txt +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/pytaskwarrior.egg-info/top_level.txt +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/adapters/__init__.py +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/config/config_store.py +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/config/uda_parser.py +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/dto/annotation_dto.py +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/dto/context_dto.py +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/dto/task_dto.py +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/enums.py +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/exceptions.py +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/py.typed +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/registry/__init__.py +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/registry/uda_registry.py +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/services/__init__.py +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/services/context_service.py +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/utils/__init__.py +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/utils/conversions.py +0 -0
- {pytaskwarrior-2.0.3 → pytaskwarrior-2.0.5}/src/taskwarrior/utils/dto_converter.py +0 -0
|
@@ -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(
|
|
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,
|
|
125
|
-
| `delete_task(
|
|
126
|
-
| `purge_task(
|
|
127
|
-
| `done_task(
|
|
128
|
-
| `start_task(
|
|
129
|
-
| `stop_task(
|
|
130
|
-
| `annotate_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
|
|
|
@@ -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,
|
|
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: {
|
|
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(
|
|
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(
|
|
274
|
-
logger.info(f"Successfully modified task with 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,
|
|
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
|
-
|
|
280
|
-
logger.debug(f"Retrieving task with ID/UUID: {
|
|
285
|
+
tid = _to_taskid(task_id)
|
|
286
|
+
logger.debug(f"Retrieving task with ID/UUID: {tid}")
|
|
281
287
|
|
|
282
|
-
args = [filter_args,
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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,
|
|
373
|
+
def get_recurring_task(self, task_id: str | int | UUID | TaskID) -> TaskOutputDTO:
|
|
368
374
|
"""Get the parent recurring task template."""
|
|
369
|
-
|
|
370
|
-
logger.debug(f"Getting recurring task with 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(
|
|
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 {
|
|
394
|
+
f"Recurring task {tid} not found as recurring, trying normal retrieval"
|
|
389
395
|
)
|
|
390
|
-
return self.get_task(
|
|
396
|
+
return self.get_task(tid)
|
|
391
397
|
|
|
392
|
-
def get_recurring_instances(self,
|
|
398
|
+
def get_recurring_instances(self, task_id: str | int | UUID | TaskID) -> list[TaskOutputDTO]:
|
|
393
399
|
"""Get all instances of a recurring task."""
|
|
394
|
-
|
|
395
|
-
logger.debug(f"Getting recurring instances for parent 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(
|
|
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,
|
|
429
|
+
def delete_task(self, task_id: str | int | UUID | TaskID) -> None:
|
|
424
430
|
"""Mark a task as deleted."""
|
|
425
|
-
task_ref = str(
|
|
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,
|
|
443
|
+
def purge_task(self, task_id: str | int | UUID | TaskID) -> None:
|
|
438
444
|
"""Permanently remove a task."""
|
|
439
|
-
task_ref = str(
|
|
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,
|
|
457
|
+
def done_task(self, task_id: str | int | UUID | TaskID) -> None:
|
|
452
458
|
"""Mark a task as completed."""
|
|
453
|
-
task_ref = str(
|
|
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,
|
|
471
|
+
def start_task(self, task_id: str | int | UUID | TaskID) -> None:
|
|
466
472
|
"""Start working on a task."""
|
|
467
|
-
task_ref = str(
|
|
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,
|
|
485
|
+
def stop_task(self, task_id: str | int | UUID | TaskID) -> None:
|
|
480
486
|
"""Stop working on a task."""
|
|
481
|
-
task_ref = str(
|
|
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,
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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,
|
|
135
|
+
return self.adapter.modify_task(task, task_id)
|
|
136
136
|
|
|
137
|
-
def get_task(self,
|
|
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
|
-
|
|
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(
|
|
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,
|
|
214
|
+
def get_recurring_task(self, task_id: TaskRef) -> TaskOutputDTO:
|
|
214
215
|
"""Get the parent recurring task template.
|
|
215
216
|
|
|
216
217
|
Args:
|
|
217
|
-
|
|
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(
|
|
226
|
+
return self.adapter.get_recurring_task(task_id)
|
|
226
227
|
|
|
227
|
-
def get_recurring_instances(self,
|
|
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
|
-
|
|
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(
|
|
240
|
+
return self.adapter.get_recurring_instances(task_id)
|
|
240
241
|
|
|
241
|
-
def delete_task(self,
|
|
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
|
-
|
|
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(
|
|
253
|
+
self.adapter.delete_task(task_id)
|
|
253
254
|
|
|
254
|
-
def purge_task(self,
|
|
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
|
-
|
|
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(
|
|
266
|
+
self.adapter.purge_task(task_id)
|
|
266
267
|
|
|
267
|
-
def done_task(self,
|
|
268
|
+
def done_task(self, task_id: TaskRef) -> None:
|
|
268
269
|
"""Mark a task as completed.
|
|
269
270
|
|
|
270
271
|
Args:
|
|
271
|
-
|
|
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(
|
|
282
|
+
self.adapter.done_task(task_id)
|
|
281
283
|
|
|
282
|
-
def start_task(self,
|
|
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
|
-
|
|
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(
|
|
295
|
+
self.adapter.start_task(task_id)
|
|
294
296
|
|
|
295
|
-
def stop_task(self,
|
|
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
|
-
|
|
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(
|
|
308
|
+
self.adapter.stop_task(task_id)
|
|
307
309
|
|
|
308
|
-
def annotate_task(self,
|
|
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
|
-
|
|
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(
|
|
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.
|
|
@@ -554,6 +556,47 @@ class TaskWarrior:
|
|
|
554
556
|
"""
|
|
555
557
|
return self.uda_service.registry.get_uda(name)
|
|
556
558
|
|
|
559
|
+
def define_uda(self, uda: UdaConfig) -> None:
|
|
560
|
+
"""Define a new UDA via the TaskWarrior facade.
|
|
561
|
+
|
|
562
|
+
Delegates to UdaService.define_uda which performs the necessary
|
|
563
|
+
TaskWarrior config writes and registers the UDA in the local registry.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
uda: The UdaConfig describing the UDA to create.
|
|
567
|
+
|
|
568
|
+
Raises:
|
|
569
|
+
TaskOperationError: If creating the UDA via the underlying adapter fails.
|
|
570
|
+
"""
|
|
571
|
+
self.uda_service.define_uda(uda)
|
|
572
|
+
|
|
573
|
+
def update_uda(self, uda: UdaConfig) -> None:
|
|
574
|
+
"""Update an existing UDA via the TaskWarrior facade.
|
|
575
|
+
|
|
576
|
+
Delegates to UdaService.update_uda.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
uda: The UdaConfig with updated fields.
|
|
580
|
+
|
|
581
|
+
Raises:
|
|
582
|
+
TaskOperationError: If applying the update fails.
|
|
583
|
+
"""
|
|
584
|
+
self.uda_service.update_uda(uda)
|
|
585
|
+
|
|
586
|
+
def delete_uda(self, uda: UdaConfig) -> None:
|
|
587
|
+
"""Delete a UDA via the TaskWarrior facade.
|
|
588
|
+
|
|
589
|
+
Delegates to UdaService.delete_uda which removes TaskWarrior config
|
|
590
|
+
keys and the UDA from the local registry.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
uda: The UdaConfig identifying the UDA to remove.
|
|
594
|
+
|
|
595
|
+
Raises:
|
|
596
|
+
TaskOperationError: If deletion fails for reasons other than missing keys.
|
|
597
|
+
"""
|
|
598
|
+
self.uda_service.delete_uda(uda)
|
|
599
|
+
|
|
557
600
|
def get_projects(self) -> list[str]:
|
|
558
601
|
"""Get all projects defined in TaskWarrior.
|
|
559
602
|
|
|
@@ -59,6 +59,16 @@ class UdaService:
|
|
|
59
59
|
|
|
60
60
|
The service executes the required `task config` commands via the adapter
|
|
61
61
|
and only updates the registry if all commands succeed.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
uda: The UdaConfig describing the UDA to create.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
TaskOperationError: If any underlying TaskWarrior config command fails.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> uda = UdaConfig(name="sev", uda_type=UdaType.STRING, label="Severity")
|
|
71
|
+
>>> service.define_uda(uda)
|
|
62
72
|
"""
|
|
63
73
|
# Build commands to define the UDA
|
|
64
74
|
field_names = uda.__class__.model_fields.keys() - {"name"}
|
|
@@ -86,6 +96,12 @@ class UdaService:
|
|
|
86
96
|
"""Update an existing UDA in TaskWarrior and in the registry.
|
|
87
97
|
|
|
88
98
|
Executes commands via adapter and updates the registry on success.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
uda: The UdaConfig with updated settings to apply.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
TaskOperationError: If applying the updated configuration fails.
|
|
89
105
|
"""
|
|
90
106
|
# For now, same as define_uda
|
|
91
107
|
self.define_uda(uda)
|
|
@@ -94,9 +110,26 @@ class UdaService:
|
|
|
94
110
|
"""Delete a UDA from TaskWarrior and remove it from the registry.
|
|
95
111
|
|
|
96
112
|
Executes `task config <key>` without a value to remove each UDA key.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
uda: The UdaConfig identifying the UDA to remove.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
TaskOperationError: If an unexpected TaskWarrior error occurs while
|
|
119
|
+
attempting to remove configuration keys (missing keys are tolerated).
|
|
97
120
|
"""
|
|
98
|
-
|
|
99
|
-
|
|
121
|
+
# Mirror define_uda: skip 'name' and map internal 'uda_type' -> TaskWarrior 'type'
|
|
122
|
+
field_names = set(uda.__class__.model_fields.keys()) - {"name"}
|
|
123
|
+
keys_to_delete: list[str] = []
|
|
124
|
+
|
|
125
|
+
if "uda_type" in field_names:
|
|
126
|
+
keys_to_delete.append("type")
|
|
127
|
+
field_names.remove("uda_type")
|
|
128
|
+
|
|
129
|
+
# delete remaining fields deterministically
|
|
130
|
+
keys_to_delete.extend(sorted(field_names))
|
|
131
|
+
|
|
132
|
+
for key in keys_to_delete:
|
|
100
133
|
cmd = ["config", f"uda.{uda.name}.{key}"]
|
|
101
134
|
result = self.adapter.run_task_command(cmd)
|
|
102
135
|
if getattr(result, "returncode", 0) != 0:
|
|
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
|
|
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
|