pytaskwarrior 1.2.0__tar.gz → 2.0.0__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-1.2.0 → pytaskwarrior-2.0.0}/PKG-INFO +1 -1
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/README.md +7 -6
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/pyproject.toml +1 -1
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/pytaskwarrior.egg-info/PKG-INFO +1 -1
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/pytaskwarrior.egg-info/SOURCES.txt +1 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/__init__.py +0 -1
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/adapters/taskwarrior_adapter.py +4 -13
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/config/config_store.py +26 -4
- pytaskwarrior-2.0.0/src/taskwarrior/config/uda_parser.py +65 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/dto/__init__.py +1 -1
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/dto/annotation_dto.py +1 -3
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/dto/task_dto.py +2 -6
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/dto/uda_dto.py +5 -5
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/exceptions.py +1 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/main.py +16 -19
- pytaskwarrior-2.0.0/src/taskwarrior/registry/uda_registry.py +70 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/services/context_service.py +26 -27
- pytaskwarrior-2.0.0/src/taskwarrior/services/uda_service.py +109 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/utils/conversions.py +0 -1
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/utils/dto_converter.py +12 -1
- pytaskwarrior-1.2.0/src/taskwarrior/registry/uda_registry.py +0 -168
- pytaskwarrior-1.2.0/src/taskwarrior/services/uda_service.py +0 -92
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/LICENSE +0 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/PYPI_README.md +0 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/setup.cfg +0 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/__init__.py +0 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/pytaskwarrior.egg-info/dependency_links.txt +0 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/pytaskwarrior.egg-info/requires.txt +0 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/pytaskwarrior.egg-info/top_level.txt +0 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/adapters/__init__.py +0 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/dto/context_dto.py +0 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/enums.py +0 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/py.typed +0 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/registry/__init__.py +0 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/services/__init__.py +0 -0
- {pytaskwarrior-1.2.0 → pytaskwarrior-2.0.0}/src/taskwarrior/utils/__init__.py +0 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
A modern Python wrapper for [TaskWarrior](https://taskwarrior.org/) v3.4, the command-line task management tool.
|
|
11
11
|
|
|
12
|
-
**
|
|
12
|
+
**v2.0.0**: Major release with breaking API changes (Context.define now accepts ContextDTO; UdaConfig.type → UdaConfig.uda_type). All tests passing and documentation updated.
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
@@ -31,7 +31,7 @@ A modern Python wrapper for [TaskWarrior](https://taskwarrior.org/) v3.4, the co
|
|
|
31
31
|
## Installation
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
pip install pytaskwarrior==
|
|
34
|
+
pip install pytaskwarrior==2.0.0
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
Or install from source:
|
|
@@ -132,7 +132,7 @@ tw = TaskWarrior(
|
|
|
132
132
|
|
|
133
133
|
| Method | Description |
|
|
134
134
|
|--------|-------------|
|
|
135
|
-
| `define_context(
|
|
135
|
+
| `define_context(ctx: ContextDTO)` | Create a context from a ContextDTO (name, read_filter, write_filter) |
|
|
136
136
|
| `apply_context(name)` | Activate a context |
|
|
137
137
|
| `unset_context()` | Deactivate current context |
|
|
138
138
|
| `get_contexts()` | List all contexts |
|
|
@@ -144,7 +144,7 @@ tw = TaskWarrior(
|
|
|
144
144
|
|
|
145
145
|
| Method | Description |
|
|
146
146
|
|--------|-------------|
|
|
147
|
-
| `is_sync_configured()` | Return `True` if any `sync.*` key is present in
|
|
147
|
+
| `is_sync_configured()` | Return `True` if any `sync.*` key is present in configuration (ConfigStore). |
|
|
148
148
|
| `synchronize()` | Run `task sync`; raises `TaskSyncError` if not configured or sync fails. |
|
|
149
149
|
|
|
150
150
|
### Exceptions
|
|
@@ -244,9 +244,10 @@ RecurrencePeriod.YEARLY
|
|
|
244
244
|
### Working with Contexts
|
|
245
245
|
|
|
246
246
|
```python
|
|
247
|
+
from taskwarrior import ContextDTO
|
|
247
248
|
# Define contexts for different workflows
|
|
248
|
-
tw.define_context("work", read_filter="project:work or +urgent", write_filter="project:work or +urgent")
|
|
249
|
-
tw.define_context("home", read_filter="project:home or project:personal", write_filter="project:home or project:personal")
|
|
249
|
+
tw.define_context(ContextDTO(name="work", read_filter="project:work or +urgent", write_filter="project:work or +urgent"))
|
|
250
|
+
tw.define_context(ContextDTO(name="home", read_filter="project:home or project:personal", write_filter="project:home or project:personal"))
|
|
250
251
|
|
|
251
252
|
# Switch to work context
|
|
252
253
|
tw.apply_context("work")
|
|
@@ -16,6 +16,7 @@ src/taskwarrior/py.typed
|
|
|
16
16
|
src/taskwarrior/adapters/__init__.py
|
|
17
17
|
src/taskwarrior/adapters/taskwarrior_adapter.py
|
|
18
18
|
src/taskwarrior/config/config_store.py
|
|
19
|
+
src/taskwarrior/config/uda_parser.py
|
|
19
20
|
src/taskwarrior/dto/__init__.py
|
|
20
21
|
src/taskwarrior/dto/annotation_dto.py
|
|
21
22
|
src/taskwarrior/dto/context_dto.py
|
|
@@ -28,7 +28,6 @@ logger = logging.getLogger(__name__)
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class TaskWarriorAdapter:
|
|
31
|
-
|
|
32
31
|
"""Low-level adapter for TaskWarrior CLI commands.
|
|
33
32
|
|
|
34
33
|
This class handles direct communication with the TaskWarrior binary,
|
|
@@ -134,9 +133,7 @@ class TaskWarriorAdapter:
|
|
|
134
133
|
)
|
|
135
134
|
result = self.run_task_command(["sync"])
|
|
136
135
|
if result.returncode != 0:
|
|
137
|
-
raise TaskSyncError(
|
|
138
|
-
f"Synchronization failed: {result.stderr or result.stdout}"
|
|
139
|
-
)
|
|
136
|
+
raise TaskSyncError(f"Synchronization failed: {result.stderr or result.stdout}")
|
|
140
137
|
|
|
141
138
|
@staticmethod
|
|
142
139
|
def _wrap_filter(f: str) -> str:
|
|
@@ -267,13 +264,9 @@ class TaskWarriorAdapter:
|
|
|
267
264
|
)
|
|
268
265
|
except json.JSONDecodeError as e:
|
|
269
266
|
logger.error(f"Failed to parse JSON response: {e}")
|
|
270
|
-
raise TaskWarriorError(
|
|
271
|
-
f"Invalid response from TaskWarrior: {result.stdout}"
|
|
272
|
-
) from e
|
|
267
|
+
raise TaskWarriorError(f"Invalid response from TaskWarrior: {result.stdout}") from e
|
|
273
268
|
else:
|
|
274
|
-
raise TaskNotFound(
|
|
275
|
-
f"Task ID/UUID {task_id_or_uuid} not found"
|
|
276
|
-
)
|
|
269
|
+
raise TaskNotFound(f"Task ID/UUID {task_id_or_uuid} not found")
|
|
277
270
|
|
|
278
271
|
def get_tasks(
|
|
279
272
|
self,
|
|
@@ -351,9 +344,7 @@ class TaskWarriorAdapter:
|
|
|
351
344
|
tasks_data = json.loads(result.stdout)
|
|
352
345
|
except json.JSONDecodeError as e:
|
|
353
346
|
logger.error(f"Failed to parse JSON response: {e}")
|
|
354
|
-
raise TaskWarriorError(
|
|
355
|
-
f"Invalid response from TaskWarrior: {result.stdout}"
|
|
356
|
-
) from e
|
|
347
|
+
raise TaskWarriorError(f"Invalid response from TaskWarrior: {result.stdout}") from e
|
|
357
348
|
if tasks_data:
|
|
358
349
|
task = TaskOutputDTO.model_validate(tasks_data[0])
|
|
359
350
|
logger.debug(f"Successfully retrieved recurring task: {task.uuid}")
|
|
@@ -7,12 +7,14 @@ from ..exceptions import TaskConfigurationError
|
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from ..dto.context_dto import ContextDTO
|
|
10
|
+
from ..dto.uda_dto import UdaConfig
|
|
10
11
|
|
|
11
12
|
DEFAULT_OPTIONS = [
|
|
12
13
|
"rc.confirmation=off",
|
|
13
14
|
"rc.bulk=0",
|
|
14
15
|
]
|
|
15
16
|
|
|
17
|
+
|
|
16
18
|
class ConfigStore:
|
|
17
19
|
"""
|
|
18
20
|
Loads and caches Taskwarrior config from taskrc. Provides access methods and refresh capability.
|
|
@@ -20,7 +22,9 @@ class ConfigStore:
|
|
|
20
22
|
|
|
21
23
|
def __init__(self, taskrc_path: str, data_location: str | None = None) -> None:
|
|
22
24
|
self._taskrc_path: Path = Path(os.path.expandvars(taskrc_path)).expanduser()
|
|
23
|
-
self._data_location: Path | None =
|
|
25
|
+
self._data_location: Path | None = (
|
|
26
|
+
Path(os.path.expandvars(data_location)).expanduser() if data_location else None
|
|
27
|
+
)
|
|
24
28
|
self._check_or_create_taskfiles()
|
|
25
29
|
self._config: dict[str, str] | None = None
|
|
26
30
|
self._load_config()
|
|
@@ -34,7 +38,7 @@ class ConfigStore:
|
|
|
34
38
|
default_content = f"""# Taskwarrior configuration file
|
|
35
39
|
# This file was automatically created by pytaskwarrior
|
|
36
40
|
# Default data location
|
|
37
|
-
rc.data.location={self._data_location or
|
|
41
|
+
rc.data.location={self._data_location or "~/.task"}
|
|
38
42
|
# Disable confirmation prompts
|
|
39
43
|
rc.confirmation=off
|
|
40
44
|
rc.bulk=0
|
|
@@ -56,11 +60,17 @@ rc.bulk=0
|
|
|
56
60
|
except FileNotFoundError as e:
|
|
57
61
|
raise TaskConfigurationError(f"Taskrc file not found: {path}") from e
|
|
58
62
|
except PermissionError as e:
|
|
59
|
-
raise TaskConfigurationError(
|
|
63
|
+
raise TaskConfigurationError(
|
|
64
|
+
f"Cannot read taskrc file (permission denied): {path}"
|
|
65
|
+
) from e
|
|
60
66
|
except OSError as e:
|
|
61
67
|
raise TaskConfigurationError(f"Failed to read taskrc file: {path}: {e}") from e
|
|
62
68
|
# Only keep blank lines, comments, or lines containing '=' (key-value)
|
|
63
|
-
filtered = [
|
|
69
|
+
filtered = [
|
|
70
|
+
line
|
|
71
|
+
for line in lines
|
|
72
|
+
if line.strip() == "" or line.strip().startswith("#") or "=" in line
|
|
73
|
+
]
|
|
64
74
|
content = "[taskrc]\n" + "".join(filtered)
|
|
65
75
|
parser.read_string(content)
|
|
66
76
|
for section in parser.sections():
|
|
@@ -128,3 +138,15 @@ rc.bulk=0
|
|
|
128
138
|
)
|
|
129
139
|
for n, filters in names.items()
|
|
130
140
|
]
|
|
141
|
+
|
|
142
|
+
def get_udas(self) -> list["UdaConfig"]:
|
|
143
|
+
"""
|
|
144
|
+
Parse and return UDAs from the cached config mapping.
|
|
145
|
+
|
|
146
|
+
Returns a list of UdaConfig objects representing UDAs defined in the
|
|
147
|
+
TaskWarrior configuration. Uses the shared uda_parser to perform parsing.
|
|
148
|
+
"""
|
|
149
|
+
# Local import to avoid module import cycles
|
|
150
|
+
from .uda_parser import parse_udas_from_mapping
|
|
151
|
+
|
|
152
|
+
return parse_udas_from_mapping(self.config)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Parser utilities to convert TaskWarrior config mappings into UdaConfig DTOs.
|
|
2
|
+
|
|
3
|
+
This module centralizes the logic that converts a mapping of configuration
|
|
4
|
+
keys (as produced by ConfigStore.config) into UdaConfig DTOs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ..dto.uda_dto import UdaConfig, UdaType
|
|
10
|
+
from ..exceptions import TaskWarriorError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_udas_from_mapping(config: dict[str, str]) -> list[UdaConfig]:
|
|
14
|
+
"""Parse UDA definitions from a config mapping.
|
|
15
|
+
|
|
16
|
+
Accepts keys like 'uda.<name>.<attr>' or 'taskrc.uda.<name>.<attr>'.
|
|
17
|
+
Returns a list of UdaConfig objects.
|
|
18
|
+
|
|
19
|
+
Raises TaskWarriorError on parsing errors.
|
|
20
|
+
"""
|
|
21
|
+
uda_groups: dict[str, dict[str, str]] = {}
|
|
22
|
+
for key, value in config.items():
|
|
23
|
+
if not key:
|
|
24
|
+
continue
|
|
25
|
+
k = key.strip()
|
|
26
|
+
# normalize 'taskrc.' prefix if present
|
|
27
|
+
if k.startswith("taskrc."):
|
|
28
|
+
k = k[len("taskrc.") :]
|
|
29
|
+
if not k.startswith("uda."):
|
|
30
|
+
continue
|
|
31
|
+
parts = k.split(".")
|
|
32
|
+
if len(parts) < 3:
|
|
33
|
+
continue
|
|
34
|
+
name, attr = parts[1], parts[2]
|
|
35
|
+
uda_groups.setdefault(name, {})[attr] = value
|
|
36
|
+
|
|
37
|
+
udas: list[UdaConfig] = []
|
|
38
|
+
for name, attrs in uda_groups.items():
|
|
39
|
+
try:
|
|
40
|
+
converted_attrs: dict[str, object] = {}
|
|
41
|
+
for attr, val in attrs.items():
|
|
42
|
+
if attr == "type":
|
|
43
|
+
try:
|
|
44
|
+
converted_attrs["uda_type"] = UdaType(val)
|
|
45
|
+
except ValueError:
|
|
46
|
+
converted_attrs["uda_type"] = UdaType(val.lower())
|
|
47
|
+
elif attr == "values":
|
|
48
|
+
converted_attrs["values"] = [v.strip() for v in val.split(",")] if val else []
|
|
49
|
+
elif attr == "coefficient":
|
|
50
|
+
try:
|
|
51
|
+
converted_attrs["coefficient"] = float(val)
|
|
52
|
+
except (TypeError, ValueError):
|
|
53
|
+
converted_attrs["coefficient"] = None
|
|
54
|
+
elif attr == "label":
|
|
55
|
+
converted_attrs["label"] = val
|
|
56
|
+
elif attr == "default":
|
|
57
|
+
converted_attrs["default"] = val
|
|
58
|
+
else:
|
|
59
|
+
converted_attrs[attr] = val
|
|
60
|
+
|
|
61
|
+
uda = UdaConfig(name=name, **converted_attrs) # type: ignore[arg-type]
|
|
62
|
+
udas.append(uda)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
raise TaskWarriorError(f"Error while parsing UDA '{name}': {e}") from e
|
|
65
|
+
return udas
|
|
@@ -2,4 +2,4 @@ from .annotation_dto import AnnotationDTO
|
|
|
2
2
|
from .context_dto import ContextDTO
|
|
3
3
|
from .task_dto import TaskInputDTO, TaskOutputDTO
|
|
4
4
|
|
|
5
|
-
__all__ = [
|
|
5
|
+
__all__ = ["AnnotationDTO", "ContextDTO", "TaskInputDTO", "TaskOutputDTO"]
|
|
@@ -30,9 +30,7 @@ class AnnotationDTO(BaseModel):
|
|
|
30
30
|
print(f"{annotation.entry}: {annotation.description}")
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
entry: datetime = Field(
|
|
34
|
-
description="Annotation creation date and time (ISO format)"
|
|
35
|
-
)
|
|
33
|
+
entry: datetime = Field(description="Annotation creation date and time (ISO format)")
|
|
36
34
|
description: str = Field(description="Annotation description")
|
|
37
35
|
|
|
38
36
|
model_config = {"populate_by_name": True, "extra": "forbid"}
|
|
@@ -69,15 +69,11 @@ class TaskInputDTO(BaseModel):
|
|
|
69
69
|
)
|
|
70
70
|
"""
|
|
71
71
|
|
|
72
|
-
description: str | None = Field(
|
|
73
|
-
default=None, description="Task description (optional)."
|
|
74
|
-
)
|
|
72
|
+
description: str | None = Field(default=None, description="Task description (optional).")
|
|
75
73
|
priority: Priority | None = Field(
|
|
76
74
|
default=None, description="Priority of the task (H, M, L, or empty)"
|
|
77
75
|
)
|
|
78
|
-
due: str | None = Field(
|
|
79
|
-
default=None, description="Due date and time for the task (ISO format)"
|
|
80
|
-
)
|
|
76
|
+
due: str | None = Field(default=None, description="Due date and time for the task (ISO format)")
|
|
81
77
|
project: str | None = Field(default=None, description="Project the task belongs to")
|
|
82
78
|
tags: list[str] = Field(
|
|
83
79
|
default_factory=list, description="List of tags associated with the task"
|
|
@@ -26,7 +26,7 @@ class UdaType(str, Enum):
|
|
|
26
26
|
|
|
27
27
|
Example:
|
|
28
28
|
>>> from taskwarrior.dto.uda_dto import UdaConfig, UdaType
|
|
29
|
-
>>> uda = UdaConfig(name="severity",
|
|
29
|
+
>>> uda = UdaConfig(name="severity", uda_type=UdaType.STRING)
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
32
|
STRING = "string"
|
|
@@ -40,11 +40,11 @@ class UdaConfig(BaseModel):
|
|
|
40
40
|
"""Data Transfer Object for User Defined Attributes (UDAs).
|
|
41
41
|
|
|
42
42
|
UDAs extend TaskWarrior with custom fields. Each UDA has a name,
|
|
43
|
-
|
|
43
|
+
uda_type, and optional configuration like allowed values or defaults.
|
|
44
44
|
|
|
45
45
|
Attributes:
|
|
46
46
|
name: Unique name for the UDA (used as the field name).
|
|
47
|
-
|
|
47
|
+
uda_type: Data type of the UDA value.
|
|
48
48
|
label: Human-readable label for display in reports.
|
|
49
49
|
values: List of allowed values (for string type with enumeration).
|
|
50
50
|
default: Default value when not specified.
|
|
@@ -56,7 +56,7 @@ class UdaConfig(BaseModel):
|
|
|
56
56
|
|
|
57
57
|
uda = UdaConfig(
|
|
58
58
|
name="severity",
|
|
59
|
-
|
|
59
|
+
uda_type=UdaType.STRING,
|
|
60
60
|
label="Severity",
|
|
61
61
|
values=["low", "medium", "high", "critical"],
|
|
62
62
|
default="medium",
|
|
@@ -66,7 +66,7 @@ class UdaConfig(BaseModel):
|
|
|
66
66
|
"""
|
|
67
67
|
|
|
68
68
|
name: str = Field(..., description="Name of the UDA")
|
|
69
|
-
|
|
69
|
+
uda_type: UdaType = Field(..., description="Data type of the UDA")
|
|
70
70
|
label: str | None = Field(default=None, description="Display label for the UDA")
|
|
71
71
|
values: list[str] | None = Field(
|
|
72
72
|
default=None, description="Allowed values for the UDA (for string types)"
|
|
@@ -91,8 +91,9 @@ class TaskWarrior:
|
|
|
91
91
|
self.context_service: ContextService = ContextService(self.adapter, self.config_store)
|
|
92
92
|
self.uda_service: UdaService = UdaService(self.adapter, self.config_store)
|
|
93
93
|
|
|
94
|
-
# Auto-load UDA definitions from
|
|
95
|
-
|
|
94
|
+
# Auto-load UDA definitions from the configured store
|
|
95
|
+
# Use the service to orchestrate loading and registry population
|
|
96
|
+
self.uda_service.load_udas_from_store()
|
|
96
97
|
|
|
97
98
|
def add_task(self, task: TaskInputDTO) -> TaskOutputDTO:
|
|
98
99
|
"""Add a new task to TaskWarrior.
|
|
@@ -321,25 +322,19 @@ class TaskWarrior:
|
|
|
321
322
|
"""
|
|
322
323
|
self.adapter.annotate_task(task_id_or_uuid, annotation)
|
|
323
324
|
|
|
324
|
-
def define_context(self, context
|
|
325
|
-
"""Define a new context
|
|
325
|
+
def define_context(self, context) -> None:
|
|
326
|
+
"""Define a new context from a ContextDTO.
|
|
326
327
|
|
|
327
|
-
|
|
328
|
-
|
|
328
|
+
The context argument must be a ContextDTO instance containing
|
|
329
|
+
name, read_filter and write_filter.
|
|
329
330
|
|
|
330
331
|
Args:
|
|
331
|
-
context:
|
|
332
|
-
read_filter: Filter applied when listing/querying tasks.
|
|
333
|
-
write_filter: Filter applied when creating or modifying tasks.
|
|
332
|
+
context: ContextDTO instance with the context definition.
|
|
334
333
|
|
|
335
334
|
Raises:
|
|
336
335
|
TaskWarriorError: If context creation fails.
|
|
337
|
-
|
|
338
|
-
Example:
|
|
339
|
-
>>> tw.define_context("work", read_filter="project:work", write_filter="project:work")
|
|
340
|
-
>>> tw.define_context("review", read_filter="+urgent or priority:H", write_filter="")
|
|
341
336
|
"""
|
|
342
|
-
self.context_service.define_context(context
|
|
337
|
+
self.context_service.define_context(context)
|
|
343
338
|
|
|
344
339
|
def apply_context(self, context: str) -> None:
|
|
345
340
|
"""Activate a context.
|
|
@@ -471,10 +466,12 @@ class TaskWarrior:
|
|
|
471
466
|
current_context = None
|
|
472
467
|
current_context_details = None
|
|
473
468
|
|
|
474
|
-
info.update(
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
469
|
+
info.update(
|
|
470
|
+
{
|
|
471
|
+
"current_context": current_context,
|
|
472
|
+
"current_context_details": current_context_details,
|
|
473
|
+
}
|
|
474
|
+
)
|
|
478
475
|
|
|
479
476
|
return info
|
|
480
477
|
|
|
@@ -526,7 +523,7 @@ class TaskWarrior:
|
|
|
526
523
|
>>> tw.reload_udas()
|
|
527
524
|
>>> names = tw.get_uda_names()
|
|
528
525
|
"""
|
|
529
|
-
self.uda_service.
|
|
526
|
+
self.uda_service.load_udas_from_store()
|
|
530
527
|
|
|
531
528
|
def get_uda_names(self) -> set[str]:
|
|
532
529
|
"""Get all defined UDA names.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Registry for User Defined Attributes (UDAs).
|
|
2
|
+
|
|
3
|
+
This module provides the UdaRegistry class for tracking and managing UDA definitions.
|
|
4
|
+
|
|
5
|
+
The registry no longer performs direct file I/O. UDA discovery is performed
|
|
6
|
+
via ConfigStore (ConfigStore.get_udas()) or by passing an in-memory config
|
|
7
|
+
mapping to `load_from_config`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from ..dto.uda_dto import UdaConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UdaRegistry:
|
|
16
|
+
"""Registry for User Defined Attributes (UDAs).
|
|
17
|
+
|
|
18
|
+
This class maintains a registry of UDA definitions loaded from in-memory
|
|
19
|
+
configuration mappings or provided by a ConfigStore instance. It intentionally
|
|
20
|
+
avoids performing direct file I/O to keep concerns separated.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> registry = UdaRegistry()
|
|
24
|
+
>>> registry.load_from_config({"uda.example.type": "string"})
|
|
25
|
+
>>> names = registry.get_uda_names()
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._udas: dict[str, UdaConfig] = {}
|
|
30
|
+
|
|
31
|
+
def register_udas(self, udas: list[UdaConfig]) -> None:
|
|
32
|
+
"""Register a list of UdaConfig objects into the registry."""
|
|
33
|
+
for uda in udas:
|
|
34
|
+
self._udas[uda.name] = uda
|
|
35
|
+
|
|
36
|
+
def load_from_config(self, config: dict[str, str]) -> None:
|
|
37
|
+
"""Load UDA definitions from an in-memory config mapping.
|
|
38
|
+
|
|
39
|
+
The config mapping should contain keys like 'uda.<name>.<attr>'.
|
|
40
|
+
This avoids direct file I/O and allows using ConfigStore.config.
|
|
41
|
+
"""
|
|
42
|
+
# Local import to avoid module import cycles
|
|
43
|
+
from ..config.uda_parser import parse_udas_from_mapping
|
|
44
|
+
|
|
45
|
+
udas = parse_udas_from_mapping(config)
|
|
46
|
+
self.register_udas(udas)
|
|
47
|
+
|
|
48
|
+
def add_uda(self, uda: UdaConfig) -> None:
|
|
49
|
+
"""Add a UDA definition to the in-memory registry (no side effects)."""
|
|
50
|
+
self._udas[uda.name] = uda
|
|
51
|
+
|
|
52
|
+
def update_uda(self, uda: UdaConfig) -> None:
|
|
53
|
+
"""Update an existing UDA definition in the registry (no side effects)."""
|
|
54
|
+
self._udas[uda.name] = uda
|
|
55
|
+
|
|
56
|
+
def remove_uda(self, name: str) -> None:
|
|
57
|
+
"""Remove a UDA definition from the registry by name (no side effects)."""
|
|
58
|
+
self._udas.pop(name, None)
|
|
59
|
+
|
|
60
|
+
def get_uda(self, name: str) -> UdaConfig | None:
|
|
61
|
+
"""Get a UDA definition by name."""
|
|
62
|
+
return self._udas.get(name)
|
|
63
|
+
|
|
64
|
+
def get_uda_names(self) -> set[str]:
|
|
65
|
+
"""Get all registered UDA names."""
|
|
66
|
+
return set(self._udas.keys())
|
|
67
|
+
|
|
68
|
+
def is_uda_field(self, field_name: str) -> bool:
|
|
69
|
+
"""Check if a field name corresponds to a registered UDA."""
|
|
70
|
+
return field_name in self._udas
|
|
@@ -4,7 +4,6 @@ This module provides the ContextService class for managing TaskWarrior
|
|
|
4
4
|
contexts (named filters).
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
|
|
8
7
|
from typing import TYPE_CHECKING
|
|
9
8
|
|
|
10
9
|
from ..adapters.taskwarrior_adapter import TaskWarriorAdapter
|
|
@@ -29,11 +28,11 @@ class ContextService:
|
|
|
29
28
|
This service is typically accessed via TaskWarrior::
|
|
30
29
|
|
|
31
30
|
tw = TaskWarrior()
|
|
32
|
-
tw.define_context("work", read_filter="project:work", write_filter="project:work")
|
|
31
|
+
tw.define_context(ContextDTO(name="work", read_filter="project:work", write_filter="project:work"))
|
|
33
32
|
tw.apply_context("work")
|
|
34
33
|
"""
|
|
35
34
|
|
|
36
|
-
def __init__(self, adapter: TaskWarriorAdapter, config_store:
|
|
35
|
+
def __init__(self, adapter: TaskWarriorAdapter, config_store: "ConfigStore") -> None:
|
|
37
36
|
"""Initialize the context service.
|
|
38
37
|
|
|
39
38
|
Args:
|
|
@@ -47,37 +46,38 @@ class ContextService:
|
|
|
47
46
|
if not name or not name.strip():
|
|
48
47
|
raise TaskValidationError("Context name cannot be empty")
|
|
49
48
|
|
|
50
|
-
def define_context(
|
|
51
|
-
|
|
52
|
-
) -> None:
|
|
53
|
-
"""Create or update a context with explicit read and write filters.
|
|
49
|
+
def define_context(self, ctx: ContextDTO) -> None:
|
|
50
|
+
"""Create or update a context from a ContextDTO.
|
|
54
51
|
|
|
55
52
|
TaskWarrior stores read and write filters separately in .taskrc.
|
|
56
|
-
|
|
53
|
+
The ContextDTO supplies both read_filter and write_filter explicitly.
|
|
57
54
|
|
|
58
55
|
Args:
|
|
59
|
-
|
|
60
|
-
read_filter: Filter applied when listing/querying tasks.
|
|
61
|
-
write_filter: Filter applied when creating or modifying tasks.
|
|
56
|
+
ctx: ContextDTO containing name, read_filter, and write_filter.
|
|
62
57
|
|
|
63
58
|
Raises:
|
|
64
59
|
TaskWarriorError: If the name is empty or creation fails.
|
|
65
60
|
|
|
66
61
|
Example:
|
|
67
|
-
>>>
|
|
68
|
-
>>> service.define_context("urgent", read_filter="+urgent", write_filter="") # read-only filter
|
|
62
|
+
>>> svc.define_context(ContextDTO(name="work", read_filter="project:work", write_filter="project:work"))
|
|
69
63
|
"""
|
|
70
|
-
self._validate_name(name)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
64
|
+
self._validate_name(ctx.name)
|
|
65
|
+
# Collect commands to be executed
|
|
66
|
+
commands = [
|
|
67
|
+
["context", "define", ctx.name, ctx.read_filter],
|
|
68
|
+
["config", f"context.{ctx.name}.write", ctx.write_filter],
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
# Execute all commands through the adapter
|
|
72
|
+
for cmd in commands:
|
|
73
|
+
result = self.adapter.run_task_command(cmd)
|
|
74
|
+
if result.returncode != 0:
|
|
75
|
+
if cmd[0] == "context" and cmd[1] == "define":
|
|
76
|
+
raise TaskWarriorError(f"Failed to define context '{ctx.name}': {result.stderr}")
|
|
77
|
+
elif cmd[0] == "config":
|
|
78
|
+
raise TaskWarriorError(
|
|
79
|
+
f"Failed to set write filter for context '{ctx.name}': {result.stderr}"
|
|
80
|
+
)
|
|
81
81
|
self.config_store.refresh()
|
|
82
82
|
|
|
83
83
|
def apply_context(self, name: str) -> None:
|
|
@@ -153,11 +153,10 @@ class ContextService:
|
|
|
153
153
|
TaskWarriorError: If the name is empty or deletion fails.
|
|
154
154
|
"""
|
|
155
155
|
self._validate_name(name)
|
|
156
|
+
# Execute command through the adapter
|
|
156
157
|
result = self.adapter.run_task_command(["context", "delete", name])
|
|
157
158
|
if result.returncode != 0:
|
|
158
|
-
raise TaskWarriorError(
|
|
159
|
-
f"Failed to delete context '{name}': {result.stderr}"
|
|
160
|
-
)
|
|
159
|
+
raise TaskWarriorError(f"Failed to delete context '{name}': {result.stderr}")
|
|
161
160
|
self.config_store.refresh()
|
|
162
161
|
|
|
163
162
|
def has_context(self, name: str) -> bool:
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""User Defined Attributes (UDA) service for TaskWarrior.
|
|
2
|
+
|
|
3
|
+
This module provides the UdaService class for managing custom task attributes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..config.config_store import ConfigStore
|
|
10
|
+
|
|
11
|
+
from ..adapters.taskwarrior_adapter import TaskWarriorAdapter
|
|
12
|
+
from ..dto.uda_dto import UdaConfig
|
|
13
|
+
from ..exceptions import TaskOperationError
|
|
14
|
+
from ..registry.uda_registry import UdaRegistry
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UdaService:
|
|
18
|
+
"""Service for managing User Defined Attributes (UDAs).
|
|
19
|
+
|
|
20
|
+
UDAs allow extending TaskWarrior with custom fields. This service
|
|
21
|
+
provides methods to define, update, and delete UDAs, delegating
|
|
22
|
+
the actual work to UdaRegistry.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
adapter: The TaskWarriorAdapter instance for CLI communication.
|
|
26
|
+
registry: The UdaRegistry for tracking defined UDAs.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
This service is typically accessed via TaskWarrior::
|
|
30
|
+
|
|
31
|
+
tw = TaskWarrior()
|
|
32
|
+
uda = UdaConfig(name="severity", uda_type=UdaType.STRING)
|
|
33
|
+
tw.uda_service.define_uda(uda)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, adapter: TaskWarriorAdapter, config_store: "ConfigStore") -> None:
|
|
37
|
+
"""Initialize the UDA service.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
adapter: The TaskWarriorAdapter to use for CLI commands.
|
|
41
|
+
config_store: The configuration store instance (required).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
self.adapter = adapter
|
|
45
|
+
self.config_store = config_store
|
|
46
|
+
self.registry = UdaRegistry()
|
|
47
|
+
|
|
48
|
+
def load_udas_from_store(self) -> None:
|
|
49
|
+
"""Load existing UDA definitions from the configured ConfigStore.
|
|
50
|
+
|
|
51
|
+
This method delegates parsing to ConfigStore.get_udas() and registers
|
|
52
|
+
the resulting UdaConfig objects in the registry (in-memory only).
|
|
53
|
+
"""
|
|
54
|
+
udas = self.config_store.get_udas()
|
|
55
|
+
self.registry.register_udas(udas)
|
|
56
|
+
|
|
57
|
+
def define_uda(self, uda: UdaConfig) -> None:
|
|
58
|
+
"""Define a new UDA in TaskWarrior and register it locally.
|
|
59
|
+
|
|
60
|
+
The service executes the required `task config` commands via the adapter
|
|
61
|
+
and only updates the registry if all commands succeed.
|
|
62
|
+
"""
|
|
63
|
+
# Build commands to define the UDA
|
|
64
|
+
field_names = uda.__class__.model_fields.keys() - {"name"}
|
|
65
|
+
# uda_type is handled first
|
|
66
|
+
commands: list[list[str]] = [["config", f"uda.{uda.name}.type", uda.uda_type.value]]
|
|
67
|
+
field_names -= {"uda_type"}
|
|
68
|
+
|
|
69
|
+
for field_name in field_names:
|
|
70
|
+
value = getattr(uda, field_name)
|
|
71
|
+
if value is not None and value != "":
|
|
72
|
+
commands.append(["config", f"uda.{uda.name}.{field_name}", str(value)])
|
|
73
|
+
|
|
74
|
+
# Execute commands via adapter; if any fail, raise and do not modify registry
|
|
75
|
+
for cmd in commands:
|
|
76
|
+
result = self.adapter.run_task_command(cmd)
|
|
77
|
+
if getattr(result, "returncode", 0) != 0:
|
|
78
|
+
stderr = str(getattr(result, "stderr", ""))
|
|
79
|
+
raise TaskOperationError(f"Failed to run task command: {cmd} -> {stderr}")
|
|
80
|
+
|
|
81
|
+
# On success, update registry
|
|
82
|
+
self.registry.add_uda(uda)
|
|
83
|
+
|
|
84
|
+
def update_uda(self, uda: UdaConfig) -> None:
|
|
85
|
+
"""Update an existing UDA in TaskWarrior and in the registry.
|
|
86
|
+
|
|
87
|
+
Executes commands via adapter and updates the registry on success.
|
|
88
|
+
"""
|
|
89
|
+
# For now, same as define_uda
|
|
90
|
+
self.define_uda(uda)
|
|
91
|
+
|
|
92
|
+
def delete_uda(self, uda: UdaConfig) -> None:
|
|
93
|
+
"""Delete a UDA from TaskWarrior and remove it from the registry.
|
|
94
|
+
|
|
95
|
+
Executes `task config <key>` without a value to remove each UDA key.
|
|
96
|
+
"""
|
|
97
|
+
field_names = uda.__class__.model_fields.keys()
|
|
98
|
+
for key in field_names:
|
|
99
|
+
cmd = ["config", f"uda.{uda.name}.{key}"]
|
|
100
|
+
result = self.adapter.run_task_command(cmd)
|
|
101
|
+
if getattr(result, "returncode", 0) != 0:
|
|
102
|
+
stderr = str(getattr(result, "stderr", ""))
|
|
103
|
+
# tolerate missing keys (idempotent deletion)
|
|
104
|
+
if "no entry named" in stderr.lower():
|
|
105
|
+
continue
|
|
106
|
+
raise TaskOperationError(f"Failed to run task command: {cmd} -> {stderr}")
|
|
107
|
+
|
|
108
|
+
# On success, remove from registry
|
|
109
|
+
self.registry.remove_uda(uda.name)
|
|
@@ -29,7 +29,18 @@ def task_output_to_input(task_output: TaskOutputDTO) -> TaskInputDTO:
|
|
|
29
29
|
>>> tw.modify_task(input_dto, uuid)
|
|
30
30
|
"""
|
|
31
31
|
data = task_output.model_dump(
|
|
32
|
-
exclude={
|
|
32
|
+
exclude={
|
|
33
|
+
"uuid",
|
|
34
|
+
"entry",
|
|
35
|
+
"start",
|
|
36
|
+
"end",
|
|
37
|
+
"modified",
|
|
38
|
+
"index",
|
|
39
|
+
"status",
|
|
40
|
+
"urgency",
|
|
41
|
+
"imask",
|
|
42
|
+
"rtype",
|
|
43
|
+
}
|
|
33
44
|
)
|
|
34
45
|
# Convert datetime fields to strings as required by TaskInputDTO
|
|
35
46
|
datetime_fields = ["due", "scheduled", "wait", "until"]
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
"""Registry for User Defined Attributes (UDAs).
|
|
2
|
-
|
|
3
|
-
This module provides the UdaRegistry singleton class for tracking
|
|
4
|
-
and managing UDA definitions.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
from ..adapters.taskwarrior_adapter import TaskWarriorAdapter
|
|
12
|
-
from ..dto.uda_dto import UdaConfig, UdaType
|
|
13
|
-
from ..exceptions import TaskConfigurationError, TaskWarriorError
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class UdaRegistry:
|
|
17
|
-
"""Registry for User Defined Attributes (UDAs).
|
|
18
|
-
|
|
19
|
-
This class maintains a registry of UDA definitions, loaded from
|
|
20
|
-
the taskrc file or defined programmatically. Each instance has its
|
|
21
|
-
own isolated state, making it safe to use with multiple TaskWarrior
|
|
22
|
-
instances.
|
|
23
|
-
|
|
24
|
-
Attributes:
|
|
25
|
-
_udas: Dictionary mapping UDA names to their definitions.
|
|
26
|
-
|
|
27
|
-
Example:
|
|
28
|
-
>>> registry = UdaRegistry()
|
|
29
|
-
>>> registry.load_from_taskrc("~/.taskrc")
|
|
30
|
-
>>> names = registry.get_uda_names()
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
def __init__(self) -> None:
|
|
34
|
-
self._udas: dict[str, UdaConfig] = {}
|
|
35
|
-
|
|
36
|
-
def load_from_taskrc(self, taskrc_file: str | Path) -> None:
|
|
37
|
-
"""Load UDA definitions from a taskrc file.
|
|
38
|
-
|
|
39
|
-
Parses the taskrc file to find all `uda.*` configuration lines
|
|
40
|
-
and creates UdaConfig objects for each discovered UDA.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
taskrc_file: Path to the taskrc configuration file.
|
|
44
|
-
|
|
45
|
-
Raises:
|
|
46
|
-
TaskWarriorError: If the file doesn't exist or parsing fails.
|
|
47
|
-
|
|
48
|
-
Example:
|
|
49
|
-
>>> registry.load_from_taskrc("/path/to/.taskrc")
|
|
50
|
-
"""
|
|
51
|
-
self._udas = {}
|
|
52
|
-
try:
|
|
53
|
-
with open(taskrc_file) as f:
|
|
54
|
-
content = f.read()
|
|
55
|
-
# Find all uda.* lines
|
|
56
|
-
uda_lines = [
|
|
57
|
-
line.strip()
|
|
58
|
-
for line in content.splitlines()
|
|
59
|
-
if line.strip().startswith("uda.")
|
|
60
|
-
]
|
|
61
|
-
# Group by UDA name
|
|
62
|
-
uda_groups: dict[str, dict[str, str]] = {}
|
|
63
|
-
for line in uda_lines:
|
|
64
|
-
if "=" not in line:
|
|
65
|
-
continue
|
|
66
|
-
key, value = line.split("=", 1)
|
|
67
|
-
parts = key.split(".")
|
|
68
|
-
if len(parts) < 3:
|
|
69
|
-
continue
|
|
70
|
-
name, attr = parts[1], parts[2]
|
|
71
|
-
if name not in uda_groups:
|
|
72
|
-
uda_groups[name] = {}
|
|
73
|
-
uda_groups[name][attr] = value.strip()
|
|
74
|
-
|
|
75
|
-
# Convert to UdaConfig objects
|
|
76
|
-
for name, attrs in uda_groups.items():
|
|
77
|
-
try:
|
|
78
|
-
# Convert type string to UdaType enum
|
|
79
|
-
converted_attrs: dict[str, object] = {}
|
|
80
|
-
for key, value in attrs.items():
|
|
81
|
-
if key == "type":
|
|
82
|
-
converted_attrs[key] = UdaType(value)
|
|
83
|
-
elif key == "values":
|
|
84
|
-
converted_attrs[key] = value.split(",") if value else []
|
|
85
|
-
elif key == "default":
|
|
86
|
-
converted_attrs[key] = value
|
|
87
|
-
else:
|
|
88
|
-
converted_attrs[key] = value
|
|
89
|
-
|
|
90
|
-
self._udas[name] = UdaConfig(name=name, **converted_attrs) # type: ignore[arg-type]
|
|
91
|
-
except Exception as e:
|
|
92
|
-
raise TaskWarriorError(f"Error while parsing {name}: {str(e)}") from e
|
|
93
|
-
|
|
94
|
-
except FileNotFoundError as e:
|
|
95
|
-
raise TaskConfigurationError(f"Taskrc file not found: {taskrc_file}") from e
|
|
96
|
-
except Exception as e:
|
|
97
|
-
raise TaskWarriorError(f"Error reading taskrc: {str(e)}") from e
|
|
98
|
-
|
|
99
|
-
def define_update_uda(self, uda: UdaConfig, adapter: TaskWarriorAdapter) -> None:
|
|
100
|
-
"""Define or update a UDA in TaskWarrior configuration.
|
|
101
|
-
|
|
102
|
-
Uses `task config` commands to set UDA properties and
|
|
103
|
-
updates the local registry.
|
|
104
|
-
|
|
105
|
-
Args:
|
|
106
|
-
uda: The UDA definition to create or update.
|
|
107
|
-
adapter: The TaskWarriorAdapter for executing commands.
|
|
108
|
-
"""
|
|
109
|
-
# Get all field names from UdaConfig
|
|
110
|
-
field_names = uda.__class__.model_fields.keys() - {"name"}
|
|
111
|
-
# Process the type
|
|
112
|
-
field_names -= {"type"}
|
|
113
|
-
adapter.run_task_command(
|
|
114
|
-
["config", f"uda.{uda.name}.type", uda.type.value]
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
# Process each field that has a value
|
|
118
|
-
for field_name in field_names:
|
|
119
|
-
value = getattr(uda, field_name)
|
|
120
|
-
if value is not None and value != "":
|
|
121
|
-
config_key = f"uda.{uda.name}.{field_name}"
|
|
122
|
-
adapter.run_task_command(["config", config_key, str(value)])
|
|
123
|
-
self._udas.update({uda.name: uda})
|
|
124
|
-
|
|
125
|
-
def delete_uda(self, uda: UdaConfig, adapter: TaskWarriorAdapter) -> None:
|
|
126
|
-
"""Delete a UDA from TaskWarrior configuration.
|
|
127
|
-
|
|
128
|
-
Clears all UDA configuration entries and removes it from the registry.
|
|
129
|
-
|
|
130
|
-
Args:
|
|
131
|
-
uda: The UDA to delete.
|
|
132
|
-
adapter: The TaskWarriorAdapter for executing commands.
|
|
133
|
-
"""
|
|
134
|
-
# Clear all UDA configuration entries by setting them to empty strings
|
|
135
|
-
field_names = uda.__class__.model_fields.keys()
|
|
136
|
-
for key in field_names:
|
|
137
|
-
adapter.run_task_command(["config", f"uda.{uda.name}.{key}"])
|
|
138
|
-
self._udas.pop(uda.name)
|
|
139
|
-
|
|
140
|
-
def get_uda(self, name: str) -> UdaConfig | None:
|
|
141
|
-
"""Get a UDA definition by name.
|
|
142
|
-
|
|
143
|
-
Args:
|
|
144
|
-
name: The name of the UDA to retrieve.
|
|
145
|
-
|
|
146
|
-
Returns:
|
|
147
|
-
The UdaConfig if found, None otherwise.
|
|
148
|
-
"""
|
|
149
|
-
return self._udas.get(name)
|
|
150
|
-
|
|
151
|
-
def get_uda_names(self) -> set[str]:
|
|
152
|
-
"""Get all registered UDA names.
|
|
153
|
-
|
|
154
|
-
Returns:
|
|
155
|
-
Set of UDA names currently in the registry.
|
|
156
|
-
"""
|
|
157
|
-
return set(self._udas.keys())
|
|
158
|
-
|
|
159
|
-
def is_uda_field(self, field_name: str) -> bool:
|
|
160
|
-
"""Check if a field name corresponds to a registered UDA.
|
|
161
|
-
|
|
162
|
-
Args:
|
|
163
|
-
field_name: The field name to check.
|
|
164
|
-
|
|
165
|
-
Returns:
|
|
166
|
-
True if the field is a registered UDA, False otherwise.
|
|
167
|
-
"""
|
|
168
|
-
return field_name in self._udas
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
"""User Defined Attributes (UDA) service for TaskWarrior.
|
|
2
|
-
|
|
3
|
-
This module provides the UdaService class for managing custom task attributes.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
7
|
-
|
|
8
|
-
from ..adapters.taskwarrior_adapter import TaskWarriorAdapter
|
|
9
|
-
from ..dto.uda_dto import UdaConfig
|
|
10
|
-
from ..registry.uda_registry import UdaRegistry
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from ..config.config_store import ConfigStore
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class UdaService:
|
|
17
|
-
"""Service for managing User Defined Attributes (UDAs).
|
|
18
|
-
|
|
19
|
-
UDAs allow extending TaskWarrior with custom fields. This service
|
|
20
|
-
provides methods to define, update, and delete UDAs, delegating
|
|
21
|
-
the actual work to UdaRegistry.
|
|
22
|
-
|
|
23
|
-
Attributes:
|
|
24
|
-
adapter: The TaskWarriorAdapter instance for CLI communication.
|
|
25
|
-
registry: The UdaRegistry for tracking defined UDAs.
|
|
26
|
-
|
|
27
|
-
Example:
|
|
28
|
-
This service is typically accessed via TaskWarrior::
|
|
29
|
-
|
|
30
|
-
tw = TaskWarrior()
|
|
31
|
-
uda = UdaConfig(name="severity", type=UdaType.STRING)
|
|
32
|
-
tw.uda_service.define_uda(uda)
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
def __init__(self, adapter: TaskWarriorAdapter, config_store: 'ConfigStore') -> None:
|
|
36
|
-
"""Initialize the UDA service.
|
|
37
|
-
|
|
38
|
-
Args:
|
|
39
|
-
adapter: The TaskWarriorAdapter to use for CLI commands.
|
|
40
|
-
config_store: The configuration store instance (required).
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
self.adapter = adapter
|
|
44
|
-
self.config_store = config_store
|
|
45
|
-
self.registry = UdaRegistry()
|
|
46
|
-
|
|
47
|
-
def load_udas_from_taskrc(self) -> None:
|
|
48
|
-
"""Load existing UDA definitions from the taskrc file.
|
|
49
|
-
|
|
50
|
-
Parses the taskrc file to discover and register any UDAs
|
|
51
|
-
that have been previously defined.
|
|
52
|
-
"""
|
|
53
|
-
self.registry.load_from_taskrc(self.config_store._taskrc_path)
|
|
54
|
-
|
|
55
|
-
def define_uda(self, uda: UdaConfig) -> None:
|
|
56
|
-
"""Define a new UDA in TaskWarrior.
|
|
57
|
-
|
|
58
|
-
Creates the UDA configuration in TaskWarrior and registers
|
|
59
|
-
it in the local registry.
|
|
60
|
-
|
|
61
|
-
Args:
|
|
62
|
-
uda: The UDA definition to create.
|
|
63
|
-
|
|
64
|
-
Example:
|
|
65
|
-
>>> uda = UdaConfig(
|
|
66
|
-
... name="severity",
|
|
67
|
-
... type=UdaType.STRING,
|
|
68
|
-
... values=["low", "medium", "high"]
|
|
69
|
-
... )
|
|
70
|
-
>>> service.define_uda(uda)
|
|
71
|
-
"""
|
|
72
|
-
self.registry.define_update_uda(uda, self.adapter)
|
|
73
|
-
|
|
74
|
-
def update_uda(self, uda: UdaConfig) -> None:
|
|
75
|
-
"""Update an existing UDA definition.
|
|
76
|
-
|
|
77
|
-
Modifies the UDA configuration in TaskWarrior.
|
|
78
|
-
|
|
79
|
-
Args:
|
|
80
|
-
uda: The updated UDA definition.
|
|
81
|
-
"""
|
|
82
|
-
self.registry.define_update_uda(uda, self.adapter)
|
|
83
|
-
|
|
84
|
-
def delete_uda(self, uda: UdaConfig) -> None:
|
|
85
|
-
"""Delete a UDA from TaskWarrior.
|
|
86
|
-
|
|
87
|
-
Removes the UDA configuration and unregisters it.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
uda: The UDA to delete.
|
|
91
|
-
"""
|
|
92
|
-
self.registry.delete_uda(uda, self.adapter)
|
|
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
|