pytaskwarrior 3.0.0a2__tar.gz → 3.0.0a5__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-3.0.0a2/src/pytaskwarrior.egg-info → pytaskwarrior-3.0.0a5}/PKG-INFO +2 -2
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/README.md +24 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/pyproject.toml +2 -3
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5/src/pytaskwarrior.egg-info}/PKG-INFO +2 -2
- pytaskwarrior-3.0.0a5/src/pytaskwarrior.egg-info/requires.txt +2 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/adapters/taskchampion_adapter.py +85 -16
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/adapters/taskwarrior_adapter.py +16 -13
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/config/config_store.py +65 -1
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/main.py +26 -16
- pytaskwarrior-3.0.0a2/src/pytaskwarrior.egg-info/requires.txt +0 -2
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/LICENSE +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/PYPI_README.md +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/setup.cfg +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/__init__.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/pytaskwarrior.egg-info/SOURCES.txt +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/pytaskwarrior.egg-info/dependency_links.txt +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/pytaskwarrior.egg-info/top_level.txt +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/__init__.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/adapters/__init__.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/adapters/tc_converter.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/adapters/tc_filter.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/config/uda_parser.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/dto/__init__.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/dto/annotation_dto.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/dto/context_dto.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/dto/task_dto.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/dto/task_id.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/dto/uda_dto.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/enums.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/exceptions.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/py.typed +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/registry/__init__.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/registry/uda_registry.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/services/__init__.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/services/context_service.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/services/uda_service.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/utils/__init__.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/utils/conversions.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/utils/date_resolver.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/utils/dto_converter.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/utils/virtual_tags.py +0 -0
- {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/tests/test_main_get_udas.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytaskwarrior
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0a5
|
|
4
4
|
Summary: Taskwarrior wrapper python module
|
|
5
5
|
Author-email: sznicolas <sznicolas@users.noreply.github.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,7 +21,7 @@ Requires-Python: >=3.12
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: pydantic>=2.11.7
|
|
24
|
-
Requires-Dist: taskchampion3-py-dev>=3.0.1.
|
|
24
|
+
Requires-Dist: taskchampion3-py-dev>=3.0.1.2a1
|
|
25
25
|
Dynamic: license-file
|
|
26
26
|
|
|
27
27
|
# pytaskwarrior
|
|
@@ -88,6 +88,30 @@ tw = TaskWarrior(
|
|
|
88
88
|
)
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
### Live configuration updates
|
|
92
|
+
|
|
93
|
+
`config_store` is the live interface to the taskrc file. Changes made via
|
|
94
|
+
`set_value()` or `set_sync_config()` are immediately effective on the next
|
|
95
|
+
adapter call — no restart or adapter recreation required.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
tw = TaskWarrior()
|
|
99
|
+
|
|
100
|
+
# Configure remote sync at runtime
|
|
101
|
+
tw.config_store.set_value("sync.server.origin", "https://sync.example.com")
|
|
102
|
+
tw.config_store.set_value("sync.encryption.secret", "my-passphrase")
|
|
103
|
+
tw.synchronize() # uses the new config immediately
|
|
104
|
+
|
|
105
|
+
# Or replace the whole sync block at once
|
|
106
|
+
tw.config_store.set_sync_config({
|
|
107
|
+
"sync.local.server_dir": "/mnt/shared/taskserver",
|
|
108
|
+
})
|
|
109
|
+
tw.synchronize()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
> **Note:** Changing `data_location` at runtime is not supported. Create a
|
|
113
|
+
> new `TaskWarrior` instance if you need a different data directory.
|
|
114
|
+
|
|
91
115
|
## Architecture
|
|
92
116
|
|
|
93
117
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pytaskwarrior"
|
|
3
|
-
version = "3.0.
|
|
3
|
+
version = "3.0.0a5"
|
|
4
4
|
description = "Taskwarrior wrapper python module"
|
|
5
5
|
readme = "PYPI_README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -21,8 +21,7 @@ classifiers = [
|
|
|
21
21
|
]
|
|
22
22
|
dependencies = [
|
|
23
23
|
"pydantic>=2.11.7",
|
|
24
|
-
|
|
25
|
-
"taskchampion3-py-dev>=3.0.1.1a0",
|
|
24
|
+
"taskchampion3-py-dev>=3.0.1.2a1",
|
|
26
25
|
]
|
|
27
26
|
|
|
28
27
|
[dependency-groups]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytaskwarrior
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0a5
|
|
4
4
|
Summary: Taskwarrior wrapper python module
|
|
5
5
|
Author-email: sznicolas <sznicolas@users.noreply.github.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -21,7 +21,7 @@ Requires-Python: >=3.12
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
23
|
Requires-Dist: pydantic>=2.11.7
|
|
24
|
-
Requires-Dist: taskchampion3-py-dev>=3.0.1.
|
|
24
|
+
Requires-Dist: taskchampion3-py-dev>=3.0.1.2a1
|
|
25
25
|
Dynamic: license-file
|
|
26
26
|
|
|
27
27
|
# pytaskwarrior
|
{pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/adapters/taskchampion_adapter.py
RENAMED
|
@@ -11,6 +11,16 @@ Usage::
|
|
|
11
11
|
dto = adapter.add_task(TaskInputDTO(description="Buy milk"))
|
|
12
12
|
print(dto.uuid, dto.index)
|
|
13
13
|
|
|
14
|
+
Live configuration updates
|
|
15
|
+
--------------------------
|
|
16
|
+
Pass a :class:`~taskwarrior.config.config_store.ConfigStore` instance to keep
|
|
17
|
+
sync parameters in sync with the taskrc file at all times::
|
|
18
|
+
|
|
19
|
+
adapter = TaskChampionAdapter(config_store=my_config_store)
|
|
20
|
+
# Later — no adapter recreation required:
|
|
21
|
+
my_config_store.set_value("sync.server.origin", "https://sync.example.com")
|
|
22
|
+
adapter.synchronize() # picks up the new URL automatically
|
|
23
|
+
|
|
14
24
|
Limitations vs :class:`~taskwarrior.adapters.taskwarrior_adapter.TaskWarriorAdapter`
|
|
15
25
|
-------------------------------------------------------------------------------------
|
|
16
26
|
* :meth:`task_calc` raises :exc:`NotImplementedError` — no TW date parser.
|
|
@@ -19,7 +29,7 @@ Limitations vs :class:`~taskwarrior.adapters.taskwarrior_adapter.TaskWarriorAdap
|
|
|
19
29
|
supported; see :mod:`~taskwarrior.adapters.tc_filter` for what is.
|
|
20
30
|
* Urgency is always ``None`` (not computed).
|
|
21
31
|
* UDA config / context support requires separate configuration.
|
|
22
|
-
*
|
|
32
|
+
* Changing ``data_location`` at runtime requires creating a new adapter instance.
|
|
23
33
|
"""
|
|
24
34
|
|
|
25
35
|
from __future__ import annotations
|
|
@@ -37,6 +47,7 @@ from uuid import UUID
|
|
|
37
47
|
|
|
38
48
|
from taskchampion import AccessMode, Annotation, Operations, Replica, Status
|
|
39
49
|
|
|
50
|
+
from ..config.config_store import ConfigStore
|
|
40
51
|
from ..dto.task_dto import TaskInputDTO, TaskOutputDTO
|
|
41
52
|
from ..dto.task_id import TaskRef, to_taskid
|
|
42
53
|
from ..exceptions import (
|
|
@@ -105,7 +116,8 @@ class TaskChampionAdapter:
|
|
|
105
116
|
data_location:
|
|
106
117
|
Path to the taskchampion data directory (the one that contains
|
|
107
118
|
``task.sqlite``). Pass ``None`` to use a temporary in-memory
|
|
108
|
-
database (useful for tests).
|
|
119
|
+
database (useful for tests). When *config_store* is supplied and
|
|
120
|
+
*data_location* is omitted, ``config_store.data_location`` is used.
|
|
109
121
|
create_if_missing:
|
|
110
122
|
When *data_location* is given, create the directory / DB if absent.
|
|
111
123
|
access_mode:
|
|
@@ -113,18 +125,36 @@ class TaskChampionAdapter:
|
|
|
113
125
|
``AccessMode.ReadOnly`` for read-only access, which is safe to use
|
|
114
126
|
from multiple concurrent threads or processes (SQLite WAL allows
|
|
115
127
|
many concurrent readers alongside a single writer).
|
|
128
|
+
config_store:
|
|
129
|
+
When provided, sync parameters (server URL, client ID, encryption
|
|
130
|
+
secret, local server dir) are **read lazily** from the store on every
|
|
131
|
+
:meth:`synchronize` call. This means that a
|
|
132
|
+
``config_store.set_value(…)`` call is immediately reflected the next
|
|
133
|
+
time sync is invoked — no adapter recreation required. This is the
|
|
134
|
+
recommended mode when the adapter is created via
|
|
135
|
+
:class:`~taskwarrior.main.TaskWarrior`.
|
|
116
136
|
sync_server_url:
|
|
117
137
|
Remote taskchampion sync server URL (``sync.server.origin`` in taskrc).
|
|
118
|
-
|
|
138
|
+
Used only when *config_store* is ``None``.
|
|
119
139
|
sync_client_id:
|
|
120
140
|
Client identifier sent to the sync server. A random UUID is used
|
|
121
141
|
when not supplied (not recommended — prefer persisting in taskrc).
|
|
142
|
+
Used only when *config_store* is ``None``.
|
|
122
143
|
sync_encryption_secret:
|
|
123
144
|
Encryption secret for the remote sync server.
|
|
145
|
+
Used only when *config_store* is ``None``.
|
|
124
146
|
sync_local_server_dir:
|
|
125
147
|
Local directory used as a sync server (``sync.local.server_dir`` in
|
|
126
148
|
taskrc). When set, :meth:`synchronize` will use ``sync_to_local``.
|
|
127
149
|
Takes precedence over *sync_server_url*.
|
|
150
|
+
Used only when *config_store* is ``None``.
|
|
151
|
+
|
|
152
|
+
Note
|
|
153
|
+
----
|
|
154
|
+
Changing ``data_location`` at runtime requires creating a new
|
|
155
|
+
:class:`TaskChampionAdapter` instance because the underlying
|
|
156
|
+
:class:`taskchampion.Replica` (SQLite connection) is opened once at
|
|
157
|
+
construction time.
|
|
128
158
|
|
|
129
159
|
Thread safety
|
|
130
160
|
-------------
|
|
@@ -166,6 +196,7 @@ class TaskChampionAdapter:
|
|
|
166
196
|
data_location: str | Path | None = None,
|
|
167
197
|
create_if_missing: bool = True,
|
|
168
198
|
access_mode: AccessMode = AccessMode.ReadWrite,
|
|
199
|
+
config_store: ConfigStore | None = None,
|
|
169
200
|
sync_server_url: str | None = None,
|
|
170
201
|
sync_client_id: str | None = None,
|
|
171
202
|
sync_encryption_secret: str | None = None,
|
|
@@ -174,14 +205,25 @@ class TaskChampionAdapter:
|
|
|
174
205
|
self._owner_thread_id = threading.current_thread().ident
|
|
175
206
|
self._db_lock = threading.Lock()
|
|
176
207
|
self._metrics = AdapterMetrics()
|
|
177
|
-
|
|
208
|
+
|
|
209
|
+
# When a ConfigStore is provided it becomes the live source of truth for
|
|
210
|
+
# sync parameters; they are re-read on every sync() call so that
|
|
211
|
+
# config_store.set_value() changes take effect without recreating the adapter.
|
|
212
|
+
self._config_store: ConfigStore | None = config_store
|
|
213
|
+
|
|
214
|
+
effective_data_location = (
|
|
215
|
+
config_store.data_location if (config_store is not None and data_location is None) else data_location
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if effective_data_location is None:
|
|
178
219
|
self._replica = Replica.new_in_memory()
|
|
179
220
|
self._data_location: str | None = None
|
|
180
221
|
else:
|
|
181
|
-
resolved = str(Path(
|
|
222
|
+
resolved = str(Path(effective_data_location).expanduser())
|
|
182
223
|
self._replica = Replica.new_on_disk(resolved, create_if_missing, access_mode)
|
|
183
224
|
self._data_location = resolved
|
|
184
225
|
|
|
226
|
+
# Legacy / direct-construction params (used when config_store is None).
|
|
185
227
|
self._sync_local_server_dir = sync_local_server_dir
|
|
186
228
|
self._sync_server_url = sync_server_url
|
|
187
229
|
self._sync_client_id = sync_client_id or str(_uuid.uuid4())
|
|
@@ -486,22 +528,36 @@ class TaskChampionAdapter:
|
|
|
486
528
|
self._locked_call(self._synchronize_internal)
|
|
487
529
|
|
|
488
530
|
def _synchronize_internal(self) -> None:
|
|
489
|
-
if not
|
|
531
|
+
if self._config_store is not None:
|
|
532
|
+
cfg = self._config_store.get_sync_config()
|
|
533
|
+
sync_local = cfg.get("sync.local.server_dir")
|
|
534
|
+
sync_url = cfg.get("sync.server.origin")
|
|
535
|
+
sync_client_id = cfg.get("sync.server.client_id") or self._sync_client_id
|
|
536
|
+
sync_secret = cfg.get("sync.encryption.secret") or ""
|
|
537
|
+
is_configured = bool(sync_local or sync_url)
|
|
538
|
+
else:
|
|
539
|
+
sync_local = self._sync_local_server_dir
|
|
540
|
+
sync_url = self._sync_server_url
|
|
541
|
+
sync_client_id = self._sync_client_id
|
|
542
|
+
sync_secret = self._sync_encryption_secret
|
|
543
|
+
is_configured = self._sync_configured
|
|
544
|
+
|
|
545
|
+
if not is_configured:
|
|
490
546
|
raise TaskSyncError(
|
|
491
547
|
"No sync server configured. "
|
|
492
548
|
"Pass sync_local_server_dir or sync_server_url to TaskChampionAdapter()."
|
|
493
549
|
)
|
|
494
550
|
try:
|
|
495
|
-
if
|
|
551
|
+
if sync_local:
|
|
496
552
|
self._replica.sync_to_local(
|
|
497
|
-
str(Path(
|
|
553
|
+
str(Path(sync_local).expanduser()),
|
|
498
554
|
_AVOID_SNAPSHOTS,
|
|
499
555
|
)
|
|
500
556
|
else:
|
|
501
557
|
self._replica.sync_to_remote(
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
558
|
+
sync_url,
|
|
559
|
+
sync_client_id,
|
|
560
|
+
sync_secret,
|
|
505
561
|
_AVOID_SNAPSHOTS,
|
|
506
562
|
)
|
|
507
563
|
logger.info("Sync completed")
|
|
@@ -510,6 +566,9 @@ class TaskChampionAdapter:
|
|
|
510
566
|
|
|
511
567
|
def is_sync_configured(self) -> bool:
|
|
512
568
|
"""Return ``True`` if a sync server URL was provided."""
|
|
569
|
+
if self._config_store is not None:
|
|
570
|
+
cfg = self._config_store.get_sync_config()
|
|
571
|
+
return bool(cfg.get("sync.server.origin") or cfg.get("sync.local.server_dir"))
|
|
513
572
|
return self._sync_configured
|
|
514
573
|
|
|
515
574
|
def has_local_changes(self) -> bool:
|
|
@@ -586,17 +645,27 @@ class TaskChampionAdapter:
|
|
|
586
645
|
|
|
587
646
|
def get_sync_info(self) -> dict[str, str | None]:
|
|
588
647
|
"""Return sync configuration details."""
|
|
589
|
-
if self.
|
|
648
|
+
if self._config_store is not None:
|
|
649
|
+
cfg = self._config_store.get_sync_config()
|
|
650
|
+
sync_url = cfg.get("sync.server.origin")
|
|
651
|
+
sync_local = cfg.get("sync.local.server_dir")
|
|
652
|
+
sync_client_id = cfg.get("sync.server.client_id")
|
|
653
|
+
else:
|
|
654
|
+
sync_url = self._sync_server_url
|
|
655
|
+
sync_local = self._sync_local_server_dir
|
|
656
|
+
sync_client_id = self._sync_client_id if self._sync_server_url else None
|
|
657
|
+
|
|
658
|
+
if sync_local:
|
|
590
659
|
sync_backend: str | None = "local"
|
|
591
|
-
elif
|
|
660
|
+
elif sync_url:
|
|
592
661
|
sync_backend = "remote"
|
|
593
662
|
else:
|
|
594
663
|
sync_backend = None
|
|
595
664
|
return {
|
|
596
665
|
"sync_backend": sync_backend,
|
|
597
|
-
"sync_server_url":
|
|
598
|
-
"sync_local_server_dir":
|
|
599
|
-
"sync_client_id":
|
|
666
|
+
"sync_server_url": sync_url,
|
|
667
|
+
"sync_local_server_dir": sync_local,
|
|
668
|
+
"sync_client_id": sync_client_id,
|
|
600
669
|
}
|
|
601
670
|
|
|
602
671
|
def get_projects(self) -> list[str]:
|
{pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/adapters/taskwarrior_adapter.py
RENAMED
|
@@ -33,9 +33,11 @@ from ..utils.virtual_tags import TASKWARRIOR_VIRTUAL_TAG_SET, TASKWARRIOR_VIRTUA
|
|
|
33
33
|
class TaskWarriorAdapter:
|
|
34
34
|
"""Low-level adapter for TaskWarrior CLI commands.
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
Communicates with the TaskWarrior binary via subprocess. Configuration
|
|
37
|
+
(CLI options, data location, sync settings) is read **lazily** from the
|
|
38
|
+
injected :class:`~taskwarrior.config.config_store.ConfigStore` on every
|
|
39
|
+
call, so a ``config_store.set_value(…)`` change is automatically reflected
|
|
40
|
+
without recreating the adapter.
|
|
39
41
|
|
|
40
42
|
Attributes:
|
|
41
43
|
task_cmd: Path to the TaskWarrior binary.
|
|
@@ -49,22 +51,23 @@ class TaskWarriorAdapter:
|
|
|
49
51
|
"""Initialize the adapter.
|
|
50
52
|
|
|
51
53
|
Args:
|
|
54
|
+
config_store: Live configuration store. CLI options, data
|
|
55
|
+
location, and sync settings are re-read from this store on
|
|
56
|
+
every operation so that runtime changes to the taskrc are
|
|
57
|
+
immediately effective.
|
|
52
58
|
task_cmd: TaskWarrior binary name or path.
|
|
53
|
-
config_store: The configuration store instance (required).
|
|
54
59
|
|
|
55
60
|
Raises:
|
|
56
61
|
TaskConfigurationError: If TaskWarrior binary not found.
|
|
57
62
|
"""
|
|
58
63
|
|
|
59
64
|
self.task_cmd: Path = self._check_binary_path(task_cmd)
|
|
60
|
-
self.
|
|
61
|
-
self._sync_configured: bool = bool(config_store.get_sync_config())
|
|
62
|
-
self._data_location: str = str(config_store.data_location)
|
|
65
|
+
self._config_store: ConfigStore = config_store
|
|
63
66
|
|
|
64
67
|
@property
|
|
65
68
|
def cli_options(self) -> list[str]:
|
|
66
|
-
"""Public accessor for CLI options."""
|
|
67
|
-
return self.
|
|
69
|
+
"""Public accessor for CLI options (always reflects current config)."""
|
|
70
|
+
return self._config_store.cli_options
|
|
68
71
|
|
|
69
72
|
def _check_binary_path(self, task_cmd: str) -> Path:
|
|
70
73
|
"""Verify TaskWarrior binary exists in PATH."""
|
|
@@ -75,7 +78,7 @@ class TaskWarriorAdapter:
|
|
|
75
78
|
|
|
76
79
|
def is_sync_configured(self) -> bool:
|
|
77
80
|
"""Return True if sync settings are present in taskrc (any ``sync.*`` key)."""
|
|
78
|
-
return self.
|
|
81
|
+
return bool(self._config_store.get_sync_config())
|
|
79
82
|
|
|
80
83
|
def has_local_changes(self) -> bool:
|
|
81
84
|
"""Always returns ``False`` for the CLI adapter.
|
|
@@ -109,7 +112,7 @@ class TaskWarriorAdapter:
|
|
|
109
112
|
cmd = [str(self.task_cmd)]
|
|
110
113
|
# Options (rc:...) must come before command and filter arguments so they are applied properly.
|
|
111
114
|
if not no_opt:
|
|
112
|
-
cmd.extend(self.
|
|
115
|
+
cmd.extend(self._config_store.cli_options)
|
|
113
116
|
cmd.extend(args)
|
|
114
117
|
logger.debug(f"Running command: {' '.join(cmd)}")
|
|
115
118
|
|
|
@@ -147,7 +150,7 @@ class TaskWarriorAdapter:
|
|
|
147
150
|
TaskSyncError: If no sync settings are configured, or if the sync
|
|
148
151
|
command exits with a non-zero return code.
|
|
149
152
|
"""
|
|
150
|
-
if not self.
|
|
153
|
+
if not self.is_sync_configured():
|
|
151
154
|
raise TaskSyncError(
|
|
152
155
|
"No sync server is configured. "
|
|
153
156
|
"Add sync.* settings to your taskrc (e.g. sync.local.server_dir)."
|
|
@@ -500,7 +503,7 @@ class TaskWarriorAdapter:
|
|
|
500
503
|
|
|
501
504
|
def get_data_location(self) -> str | None:
|
|
502
505
|
"""Return the TaskWarrior data directory path."""
|
|
503
|
-
return self.
|
|
506
|
+
return str(self._config_store.data_location)
|
|
504
507
|
|
|
505
508
|
def get_projects(self) -> list[str]:
|
|
506
509
|
"""Get all projects defined in TaskWarrior.
|
|
@@ -126,6 +126,60 @@ rc.bulk=0
|
|
|
126
126
|
# Accept both 'sync.' and 'taskrc.sync.' keys for compatibility
|
|
127
127
|
return {k: v for k, v in self.config.items() if k.startswith("sync.")}
|
|
128
128
|
|
|
129
|
+
def set_sync_config(self, config: dict[str, str | None]) -> None:
|
|
130
|
+
"""Write (or clear) the ``sync.*`` section of the taskrc file.
|
|
131
|
+
|
|
132
|
+
This is the write-symmetric counterpart of :meth:`get_sync_config`. It
|
|
133
|
+
**replaces** the current ``sync.*`` keys entirely: keys present in the
|
|
134
|
+
file but absent from *config* are deleted; keys provided in *config*
|
|
135
|
+
are written (or removed when their value is ``None``).
|
|
136
|
+
|
|
137
|
+
Keys may be supplied with or without the ``"sync."`` prefix — the prefix
|
|
138
|
+
is added automatically when missing.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
config: Mapping of sync key → value. Pass ``None`` as the value
|
|
142
|
+
to explicitly delete a key (e.g.
|
|
143
|
+
``{"sync.encryption.secret": None}``). Pass an empty dict to
|
|
144
|
+
wipe all ``sync.*`` keys.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
TaskConfigurationError: If the taskrc file cannot be read or
|
|
148
|
+
written.
|
|
149
|
+
|
|
150
|
+
Example::
|
|
151
|
+
|
|
152
|
+
tw.config_store.set_sync_config({
|
|
153
|
+
"sync.server.origin": "https://taskchampion.example.com",
|
|
154
|
+
"sync.server.client_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
155
|
+
"sync.encryption.secret": "my-passphrase",
|
|
156
|
+
})
|
|
157
|
+
# Replace with local sync, removing any previous remote config:
|
|
158
|
+
tw.config_store.set_sync_config({
|
|
159
|
+
"sync.local.server_dir": "/mnt/shared/taskserver",
|
|
160
|
+
})
|
|
161
|
+
"""
|
|
162
|
+
# Normalise keys so callers may omit the "sync." prefix.
|
|
163
|
+
normalised: dict[str, str | None] = {}
|
|
164
|
+
for k, v in config.items():
|
|
165
|
+
full_key = k if k.startswith("sync.") else f"sync.{k}"
|
|
166
|
+
normalised[full_key] = v
|
|
167
|
+
|
|
168
|
+
# Delete keys currently in the file that are not present in the new config.
|
|
169
|
+
for existing_key in list(self.get_sync_config().keys()):
|
|
170
|
+
if existing_key not in normalised:
|
|
171
|
+
self.delete_value(existing_key)
|
|
172
|
+
|
|
173
|
+
# Write or delete each supplied key.
|
|
174
|
+
for key, value in normalised.items():
|
|
175
|
+
if value is None:
|
|
176
|
+
self.delete_value(key)
|
|
177
|
+
else:
|
|
178
|
+
self.set_value(key, value)
|
|
179
|
+
|
|
180
|
+
# Final refresh ensures the in-memory cache reflects the final state.
|
|
181
|
+
self.refresh()
|
|
182
|
+
|
|
129
183
|
def get_contexts_config(self) -> dict[str, str]:
|
|
130
184
|
# Extract context config directly from self.config
|
|
131
185
|
return {k: v for k, v in self.config.items() if k.startswith("context.")}
|
|
@@ -162,13 +216,23 @@ rc.bulk=0
|
|
|
162
216
|
``key=value`` is appended at the end. The in-memory cache is refreshed
|
|
163
217
|
automatically after the write.
|
|
164
218
|
|
|
219
|
+
Because both :class:`~taskwarrior.adapters.taskchampion_adapter.TaskChampionAdapter`
|
|
220
|
+
and :class:`~taskwarrior.adapters.taskwarrior_adapter.TaskWarriorAdapter`
|
|
221
|
+
read configuration lazily from this store, changes are effective on the
|
|
222
|
+
**next** adapter call without requiring an adapter restart.
|
|
223
|
+
|
|
165
224
|
Args:
|
|
166
|
-
key: Dotted taskrc key (e.g. ``"
|
|
225
|
+
key: Dotted taskrc key (e.g. ``"sync.server.origin"``).
|
|
167
226
|
value: New value (may be empty string to clear the value without
|
|
168
227
|
removing the key).
|
|
169
228
|
|
|
170
229
|
Raises:
|
|
171
230
|
TaskConfigurationError: If the file cannot be written.
|
|
231
|
+
|
|
232
|
+
Example::
|
|
233
|
+
|
|
234
|
+
tw.config_store.set_value("sync.server.origin", "https://sync.example.com")
|
|
235
|
+
tw.synchronize() # picks up the new URL immediately
|
|
172
236
|
"""
|
|
173
237
|
pattern = re.compile(r"^\s*" + re.escape(key) + r"\s*=.*$", re.IGNORECASE)
|
|
174
238
|
try:
|
|
@@ -31,7 +31,21 @@ class TaskWarrior:
|
|
|
31
31
|
``task_cmd="task"`` (or any path to the binary) to use the classic CLI
|
|
32
32
|
adapter instead.
|
|
33
33
|
|
|
34
|
+
Both adapters read configuration **lazily** from
|
|
35
|
+
:attr:`config_store`, so changes made via
|
|
36
|
+
``tw.config_store.set_value(…)`` are immediately effective on the next
|
|
37
|
+
adapter call — no adapter recreation required.
|
|
38
|
+
|
|
39
|
+
.. note::
|
|
40
|
+
Changing ``data_location`` at runtime is not supported; create a new
|
|
41
|
+
:class:`TaskWarrior` instance in that case.
|
|
42
|
+
|
|
34
43
|
Attributes:
|
|
44
|
+
config_store: Live view of the taskrc configuration. Use
|
|
45
|
+
:meth:`~taskwarrior.config.config_store.ConfigStore.set_value`,
|
|
46
|
+
:meth:`~taskwarrior.config.config_store.ConfigStore.delete_value`,
|
|
47
|
+
and :meth:`~taskwarrior.config.config_store.ConfigStore.set_sync_config`
|
|
48
|
+
to modify settings at runtime.
|
|
35
49
|
adapter: The underlying adapter instance (TaskChampion or CLI).
|
|
36
50
|
context_service: Service for managing contexts.
|
|
37
51
|
uda_service: Service for managing UDAs.
|
|
@@ -46,6 +60,13 @@ class TaskWarrior:
|
|
|
46
60
|
added = tw.add_task(task)
|
|
47
61
|
print(f"Added task: {added.uuid}")
|
|
48
62
|
|
|
63
|
+
Live configuration update (sync config, UDA, context, …)::
|
|
64
|
+
|
|
65
|
+
tw = TaskWarrior()
|
|
66
|
+
tw.config_store.set_value("sync.server.origin", "https://sync.example.com")
|
|
67
|
+
tw.config_store.set_value("sync.encryption.secret", "my-passphrase")
|
|
68
|
+
tw.synchronize() # uses the new config immediately
|
|
69
|
+
|
|
49
70
|
Explicit CLI adapter::
|
|
50
71
|
|
|
51
72
|
tw = TaskWarrior(task_cmd="task")
|
|
@@ -108,24 +129,13 @@ class TaskWarrior:
|
|
|
108
129
|
# Default mode: TaskChampionAdapter — no binary required.
|
|
109
130
|
import uuid as _uuid
|
|
110
131
|
|
|
132
|
+
# Persist a stable client_id if remote sync is configured but none is set.
|
|
111
133
|
sync_cfg = self.config_store.get_sync_config()
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
sync_client_id = sync_cfg.get("sync.server.client_id")
|
|
115
|
-
sync_encryption_secret = sync_cfg.get("sync.encryption.secret")
|
|
134
|
+
if sync_cfg.get("sync.server.origin") and not sync_cfg.get("sync.server.client_id"):
|
|
135
|
+
self.config_store.set_value("sync.server.client_id", str(_uuid.uuid4()))
|
|
116
136
|
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
sync_client_id = str(_uuid.uuid4())
|
|
120
|
-
self.config_store.set_value("sync.server.client_id", sync_client_id)
|
|
121
|
-
|
|
122
|
-
self.adapter = TaskChampionAdapter(
|
|
123
|
-
data_location=self.config_store.data_location,
|
|
124
|
-
sync_server_url=sync_server_url,
|
|
125
|
-
sync_client_id=sync_client_id,
|
|
126
|
-
sync_encryption_secret=sync_encryption_secret,
|
|
127
|
-
sync_local_server_dir=sync_local_dir,
|
|
128
|
-
)
|
|
137
|
+
# Pass config_store directly so sync params are re-read on every sync() call.
|
|
138
|
+
self.adapter = TaskChampionAdapter(config_store=self.config_store)
|
|
129
139
|
|
|
130
140
|
self._cli_adapter: TaskWarriorAdapter | None = _cli
|
|
131
141
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/pytaskwarrior.egg-info/dependency_links.txt
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|