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.
Files changed (42) hide show
  1. {pytaskwarrior-3.0.0a2/src/pytaskwarrior.egg-info → pytaskwarrior-3.0.0a5}/PKG-INFO +2 -2
  2. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/README.md +24 -0
  3. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/pyproject.toml +2 -3
  4. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5/src/pytaskwarrior.egg-info}/PKG-INFO +2 -2
  5. pytaskwarrior-3.0.0a5/src/pytaskwarrior.egg-info/requires.txt +2 -0
  6. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/adapters/taskchampion_adapter.py +85 -16
  7. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/adapters/taskwarrior_adapter.py +16 -13
  8. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/config/config_store.py +65 -1
  9. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/main.py +26 -16
  10. pytaskwarrior-3.0.0a2/src/pytaskwarrior.egg-info/requires.txt +0 -2
  11. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/LICENSE +0 -0
  12. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/PYPI_README.md +0 -0
  13. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/setup.cfg +0 -0
  14. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/__init__.py +0 -0
  15. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/pytaskwarrior.egg-info/SOURCES.txt +0 -0
  16. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/pytaskwarrior.egg-info/dependency_links.txt +0 -0
  17. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/pytaskwarrior.egg-info/top_level.txt +0 -0
  18. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/__init__.py +0 -0
  19. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/adapters/__init__.py +0 -0
  20. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/adapters/tc_converter.py +0 -0
  21. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/adapters/tc_filter.py +0 -0
  22. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/config/uda_parser.py +0 -0
  23. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/dto/__init__.py +0 -0
  24. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/dto/annotation_dto.py +0 -0
  25. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/dto/context_dto.py +0 -0
  26. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/dto/task_dto.py +0 -0
  27. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/dto/task_id.py +0 -0
  28. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/dto/uda_dto.py +0 -0
  29. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/enums.py +0 -0
  30. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/exceptions.py +0 -0
  31. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/py.typed +0 -0
  32. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/registry/__init__.py +0 -0
  33. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/registry/uda_registry.py +0 -0
  34. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/services/__init__.py +0 -0
  35. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/services/context_service.py +0 -0
  36. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/services/uda_service.py +0 -0
  37. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/utils/__init__.py +0 -0
  38. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/utils/conversions.py +0 -0
  39. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/utils/date_resolver.py +0 -0
  40. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/utils/dto_converter.py +0 -0
  41. {pytaskwarrior-3.0.0a2 → pytaskwarrior-3.0.0a5}/src/taskwarrior/utils/virtual_tags.py +0 -0
  42. {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.0a2
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.1a0
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.0a2"
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
- # "taskchampion-py-dev>=3.0.1.1a1"
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.0a2
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.1a0
24
+ Requires-Dist: taskchampion3-py-dev>=3.0.1.2a1
25
25
  Dynamic: license-file
26
26
 
27
27
  # pytaskwarrior
@@ -0,0 +1,2 @@
1
+ pydantic>=2.11.7
2
+ taskchampion3-py-dev>=3.0.1.2a1
@@ -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
- * Sync requires ``sync_server_url`` (remote) or ``sync_local_server_dir`` (local directory).
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
- When set, :meth:`synchronize` will use ``sync_to_remote``.
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
- if data_location is None:
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(data_location).expanduser())
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 self._sync_configured:
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 self._sync_local_server_dir:
551
+ if sync_local:
496
552
  self._replica.sync_to_local(
497
- str(Path(self._sync_local_server_dir).expanduser()),
553
+ str(Path(sync_local).expanduser()),
498
554
  _AVOID_SNAPSHOTS,
499
555
  )
500
556
  else:
501
557
  self._replica.sync_to_remote(
502
- self._sync_server_url,
503
- self._sync_client_id,
504
- self._sync_encryption_secret,
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._sync_local_server_dir:
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 self._sync_server_url:
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": self._sync_server_url,
598
- "sync_local_server_dir": self._sync_local_server_dir,
599
- "sync_client_id": self._sync_client_id if self._sync_server_url else None,
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]:
@@ -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
- This class handles direct communication with the TaskWarrior binary,
37
- including command execution, argument building, and response parsing.
38
- It is used internally by the TaskWarrior facade class.
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._cli_options: list[str] = config_store.cli_options
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._cli_options
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._sync_configured
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._cli_options)
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._sync_configured:
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._data_location
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. ``"uda.severity.type"``).
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
- sync_server_url = sync_cfg.get("sync.server.origin")
113
- sync_local_dir = sync_cfg.get("sync.local.server_dir")
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
- # Persist a stable client_id if remote sync is configured but none is set.
118
- if sync_server_url and not sync_client_id:
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
 
@@ -1,2 +0,0 @@
1
- pydantic>=2.11.7
2
- taskchampion3-py-dev>=3.0.1.1a0
File without changes