pytaskwarrior 3.0.0a1__tar.gz → 3.0.0a2__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.0a1/src/pytaskwarrior.egg-info → pytaskwarrior-3.0.0a2}/PKG-INFO +1 -1
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/pyproject.toml +1 -1
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2/src/pytaskwarrior.egg-info}/PKG-INFO +1 -1
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/adapters/taskchampion_adapter.py +181 -4
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/LICENSE +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/PYPI_README.md +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/README.md +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/setup.cfg +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/__init__.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/pytaskwarrior.egg-info/SOURCES.txt +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/pytaskwarrior.egg-info/dependency_links.txt +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/pytaskwarrior.egg-info/requires.txt +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/pytaskwarrior.egg-info/top_level.txt +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/__init__.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/adapters/__init__.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/adapters/taskwarrior_adapter.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/adapters/tc_converter.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/adapters/tc_filter.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/config/config_store.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/config/uda_parser.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/dto/__init__.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/dto/annotation_dto.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/dto/context_dto.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/dto/task_dto.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/dto/task_id.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/dto/uda_dto.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/enums.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/exceptions.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/main.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/py.typed +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/registry/__init__.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/registry/uda_registry.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/services/__init__.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/services/context_service.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/services/uda_service.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/utils/__init__.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/utils/conversions.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/utils/date_resolver.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/utils/dto_converter.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/utils/virtual_tags.py +0 -0
- {pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/tests/test_main_get_udas.py +0 -0
{pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/adapters/taskchampion_adapter.py
RENAMED
|
@@ -25,9 +25,14 @@ Limitations vs :class:`~taskwarrior.adapters.taskwarrior_adapter.TaskWarriorAdap
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
27
|
import logging
|
|
28
|
+
import threading
|
|
29
|
+
import time
|
|
28
30
|
import uuid as _uuid
|
|
31
|
+
from collections.abc import Callable
|
|
32
|
+
from dataclasses import dataclass, field
|
|
29
33
|
from datetime import UTC, datetime
|
|
30
34
|
from pathlib import Path
|
|
35
|
+
from typing import Any, TypeVar
|
|
31
36
|
from uuid import UUID
|
|
32
37
|
|
|
33
38
|
from taskchampion import AccessMode, Annotation, Operations, Replica, Status
|
|
@@ -49,6 +54,47 @@ logger = logging.getLogger(__name__)
|
|
|
49
54
|
|
|
50
55
|
_VERSION = "taskchampion-py/3.0.1"
|
|
51
56
|
_AVOID_SNAPSHOTS: bool = False
|
|
57
|
+
_LOCK_TIMEOUT: float = 30.0
|
|
58
|
+
_T = TypeVar("_T")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class AdapterMetrics:
|
|
63
|
+
"""Thread-safe metrics for :class:`TaskChampionAdapter` operations.
|
|
64
|
+
|
|
65
|
+
Attributes track cumulative counts and timings across all calls made
|
|
66
|
+
through :meth:`TaskChampionAdapter._locked_call`. All fields are
|
|
67
|
+
updated atomically behind an internal lock.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
calls_total: int = 0
|
|
71
|
+
errors_total: int = 0
|
|
72
|
+
wait_seconds_total: float = 0.0
|
|
73
|
+
run_seconds_total: float = 0.0
|
|
74
|
+
_lock: threading.Lock = field(default_factory=threading.Lock, init=False, repr=False)
|
|
75
|
+
|
|
76
|
+
def record(self, wait: float, run: float, *, error: bool = False) -> None:
|
|
77
|
+
"""Record timing and error status for one completed call."""
|
|
78
|
+
with self._lock:
|
|
79
|
+
self.calls_total += 1
|
|
80
|
+
self.wait_seconds_total += wait
|
|
81
|
+
self.run_seconds_total += run
|
|
82
|
+
if error:
|
|
83
|
+
self.errors_total += 1
|
|
84
|
+
|
|
85
|
+
def snapshot(self) -> dict[str, Any]:
|
|
86
|
+
"""Return a point-in-time copy of the metrics."""
|
|
87
|
+
with self._lock:
|
|
88
|
+
avg_wait = (
|
|
89
|
+
round(self.wait_seconds_total / self.calls_total, 3)
|
|
90
|
+
if self.calls_total
|
|
91
|
+
else 0.0
|
|
92
|
+
)
|
|
93
|
+
return {
|
|
94
|
+
"calls_total": self.calls_total,
|
|
95
|
+
"errors_total": self.errors_total,
|
|
96
|
+
"avg_wait_seconds": avg_wait,
|
|
97
|
+
}
|
|
52
98
|
|
|
53
99
|
|
|
54
100
|
class TaskChampionAdapter:
|
|
@@ -82,10 +128,17 @@ class TaskChampionAdapter:
|
|
|
82
128
|
|
|
83
129
|
Thread safety
|
|
84
130
|
-------------
|
|
85
|
-
Each :class:`TaskChampionAdapter` instance **
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
131
|
+
Each :class:`TaskChampionAdapter` instance is **bound to the thread that
|
|
132
|
+
created it**. The underlying :class:`taskchampion.Replica` is
|
|
133
|
+
``unsendable`` (a PyO3 constraint): calling any method from a different
|
|
134
|
+
thread raises a Rust-level :exc:`pyo3_runtime.PanicException`.
|
|
135
|
+
|
|
136
|
+
To surface this early as a clear Python error, every method that accesses
|
|
137
|
+
the :class:`~taskchampion.Replica` calls :meth:`_check_thread_affinity`
|
|
138
|
+
on entry and raises :exc:`RuntimeError` immediately if the call originates
|
|
139
|
+
from a foreign thread. A :class:`threading.Lock` is also held during each
|
|
140
|
+
operation for internal consistency (e.g. coroutines sharing the adapter on
|
|
141
|
+
the same asyncio event loop).
|
|
89
142
|
|
|
90
143
|
Concurrency patterns:
|
|
91
144
|
|
|
@@ -100,6 +153,12 @@ class TaskChampionAdapter:
|
|
|
100
153
|
readers without blocking.
|
|
101
154
|
* **In-memory mode** (``data_location=None``) — each instance is fully
|
|
102
155
|
isolated; do not share between threads.
|
|
156
|
+
|
|
157
|
+
Metrics
|
|
158
|
+
-------
|
|
159
|
+
Call :meth:`get_metrics` to retrieve a snapshot of operation counts,
|
|
160
|
+
error counts, and average wait/run times recorded by the internal
|
|
161
|
+
:class:`AdapterMetrics` instance.
|
|
103
162
|
"""
|
|
104
163
|
|
|
105
164
|
def __init__(
|
|
@@ -112,6 +171,9 @@ class TaskChampionAdapter:
|
|
|
112
171
|
sync_encryption_secret: str | None = None,
|
|
113
172
|
sync_local_server_dir: str | None = None,
|
|
114
173
|
) -> None:
|
|
174
|
+
self._owner_thread_id = threading.current_thread().ident
|
|
175
|
+
self._db_lock = threading.Lock()
|
|
176
|
+
self._metrics = AdapterMetrics()
|
|
115
177
|
if data_location is None:
|
|
116
178
|
self._replica = Replica.new_in_memory()
|
|
117
179
|
self._data_location: str | None = None
|
|
@@ -171,12 +233,64 @@ class TaskChampionAdapter:
|
|
|
171
233
|
ws = self._replica.working_set()
|
|
172
234
|
return tc_task_to_output_dto(task, ws)
|
|
173
235
|
|
|
236
|
+
def _check_thread_affinity(self) -> None:
|
|
237
|
+
"""Raise :exc:`RuntimeError` if called from a thread other than the owner.
|
|
238
|
+
|
|
239
|
+
The underlying :class:`taskchampion.Replica` is PyO3 *unsendable*:
|
|
240
|
+
calling it from a foreign thread would trigger a Rust-level panic.
|
|
241
|
+
This method turns that panic into an explicit Python error.
|
|
242
|
+
"""
|
|
243
|
+
if threading.current_thread().ident != self._owner_thread_id:
|
|
244
|
+
raise RuntimeError(
|
|
245
|
+
f"TaskChampionAdapter instance was created on thread "
|
|
246
|
+
f"{self._owner_thread_id} but is being accessed from thread "
|
|
247
|
+
f"{threading.current_thread().ident}. "
|
|
248
|
+
"The underlying taskchampion.Replica is thread-bound "
|
|
249
|
+
"(PyO3 unsendable constraint). "
|
|
250
|
+
"Create a separate TaskChampionAdapter instance per thread, "
|
|
251
|
+
"or adopt a per-request adapter pattern."
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def _locked_call(self, fn: Callable[..., _T], /, *args: Any, **kwargs: Any) -> _T:
|
|
255
|
+
"""Run *fn* under the instance lock, recording metrics.
|
|
256
|
+
|
|
257
|
+
Raises
|
|
258
|
+
------
|
|
259
|
+
RuntimeError
|
|
260
|
+
If called from a thread other than the owner thread.
|
|
261
|
+
TaskOperationError
|
|
262
|
+
If the lock cannot be acquired within :data:`_LOCK_TIMEOUT` seconds.
|
|
263
|
+
"""
|
|
264
|
+
self._check_thread_affinity()
|
|
265
|
+
t_wait_start = time.monotonic()
|
|
266
|
+
acquired = self._db_lock.acquire(timeout=_LOCK_TIMEOUT)
|
|
267
|
+
wait = time.monotonic() - t_wait_start
|
|
268
|
+
if not acquired:
|
|
269
|
+
self._metrics.record(wait=_LOCK_TIMEOUT, run=0.0, error=True)
|
|
270
|
+
raise TaskOperationError(
|
|
271
|
+
f"Lock timeout after {_LOCK_TIMEOUT}s waiting for {fn.__name__}"
|
|
272
|
+
)
|
|
273
|
+
error = False
|
|
274
|
+
t_run_start = time.monotonic()
|
|
275
|
+
try:
|
|
276
|
+
return fn(*args, **kwargs)
|
|
277
|
+
except Exception:
|
|
278
|
+
error = True
|
|
279
|
+
raise
|
|
280
|
+
finally:
|
|
281
|
+
run = time.monotonic() - t_run_start
|
|
282
|
+
self._metrics.record(wait=wait, run=run, error=error)
|
|
283
|
+
self._db_lock.release()
|
|
284
|
+
|
|
174
285
|
# ------------------------------------------------------------------
|
|
175
286
|
# CRUD
|
|
176
287
|
# ------------------------------------------------------------------
|
|
177
288
|
|
|
178
289
|
def add_task(self, task: TaskInputDTO) -> TaskOutputDTO:
|
|
179
290
|
"""Create a new task and return the persisted DTO."""
|
|
291
|
+
return self._locked_call(self._add_task_internal, task)
|
|
292
|
+
|
|
293
|
+
def _add_task_internal(self, task: TaskInputDTO) -> TaskOutputDTO:
|
|
180
294
|
if not task.description or not task.description.strip():
|
|
181
295
|
raise TaskValidationError("Task description cannot be empty")
|
|
182
296
|
|
|
@@ -193,6 +307,9 @@ class TaskChampionAdapter:
|
|
|
193
307
|
|
|
194
308
|
def modify_task(self, task: TaskInputDTO, task_id: TaskRef) -> TaskOutputDTO:
|
|
195
309
|
"""Modify an existing task and return the updated DTO."""
|
|
310
|
+
return self._locked_call(self._modify_task_internal, task, task_id)
|
|
311
|
+
|
|
312
|
+
def _modify_task_internal(self, task: TaskInputDTO, task_id: TaskRef) -> TaskOutputDTO:
|
|
196
313
|
uuid_str = self._resolve_ref(task_id)
|
|
197
314
|
tc_task = self._get_tc_task(uuid_str)
|
|
198
315
|
|
|
@@ -206,6 +323,9 @@ class TaskChampionAdapter:
|
|
|
206
323
|
|
|
207
324
|
def get_task(self, task_id: TaskRef) -> TaskOutputDTO:
|
|
208
325
|
"""Retrieve a single task by ID, index, or UUID."""
|
|
326
|
+
return self._locked_call(self._get_task_internal, task_id)
|
|
327
|
+
|
|
328
|
+
def _get_task_internal(self, task_id: TaskRef) -> TaskOutputDTO:
|
|
209
329
|
uuid_str = self._resolve_ref(task_id)
|
|
210
330
|
return self._fetch_dto(uuid_str)
|
|
211
331
|
|
|
@@ -223,6 +343,16 @@ class TaskChampionAdapter:
|
|
|
223
343
|
The filter is applied in Python; see :mod:`~taskwarrior.adapters.tc_filter`
|
|
224
344
|
for the supported syntax.
|
|
225
345
|
"""
|
|
346
|
+
return self._locked_call(
|
|
347
|
+
self._get_tasks_internal, filter, include_completed, include_deleted
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def _get_tasks_internal(
|
|
351
|
+
self,
|
|
352
|
+
filter: str = "",
|
|
353
|
+
include_completed: bool = False,
|
|
354
|
+
include_deleted: bool = False,
|
|
355
|
+
) -> list[TaskOutputDTO]:
|
|
226
356
|
if not include_completed and not include_deleted:
|
|
227
357
|
all_tasks = self._replica.pending_tasks()
|
|
228
358
|
else:
|
|
@@ -233,11 +363,17 @@ class TaskChampionAdapter:
|
|
|
233
363
|
|
|
234
364
|
def get_recurring_task(self, task_id: TaskRef) -> TaskOutputDTO:
|
|
235
365
|
"""Retrieve a recurring task template."""
|
|
366
|
+
return self._locked_call(self._get_recurring_task_internal, task_id)
|
|
367
|
+
|
|
368
|
+
def _get_recurring_task_internal(self, task_id: TaskRef) -> TaskOutputDTO:
|
|
236
369
|
uuid_str = self._resolve_ref(task_id)
|
|
237
370
|
return self._fetch_dto(uuid_str)
|
|
238
371
|
|
|
239
372
|
def get_recurring_instances(self, task_id: TaskRef) -> list[TaskOutputDTO]:
|
|
240
373
|
"""Return all child instances of a recurring task template."""
|
|
374
|
+
return self._locked_call(self._get_recurring_instances_internal, task_id)
|
|
375
|
+
|
|
376
|
+
def _get_recurring_instances_internal(self, task_id: TaskRef) -> list[TaskOutputDTO]:
|
|
241
377
|
uuid_str = self._resolve_ref(task_id)
|
|
242
378
|
all_tasks = list(self._replica.all_tasks().values())
|
|
243
379
|
ws = self._replica.working_set()
|
|
@@ -250,6 +386,9 @@ class TaskChampionAdapter:
|
|
|
250
386
|
|
|
251
387
|
def delete_task(self, task_id: TaskRef) -> None:
|
|
252
388
|
"""Mark a task as deleted (soft delete)."""
|
|
389
|
+
self._locked_call(self._delete_task_internal, task_id)
|
|
390
|
+
|
|
391
|
+
def _delete_task_internal(self, task_id: TaskRef) -> None:
|
|
253
392
|
uuid_str = self._resolve_ref(task_id)
|
|
254
393
|
tc_task = self._get_tc_task(uuid_str)
|
|
255
394
|
ops = Operations()
|
|
@@ -260,6 +399,9 @@ class TaskChampionAdapter:
|
|
|
260
399
|
|
|
261
400
|
def purge_task(self, task_id: TaskRef) -> None:
|
|
262
401
|
"""Permanently remove a task from the database."""
|
|
402
|
+
self._locked_call(self._purge_task_internal, task_id)
|
|
403
|
+
|
|
404
|
+
def _purge_task_internal(self, task_id: TaskRef) -> None:
|
|
263
405
|
uuid_str = self._resolve_ref(task_id)
|
|
264
406
|
tc_task = self._get_tc_task(uuid_str)
|
|
265
407
|
ops = Operations()
|
|
@@ -270,6 +412,9 @@ class TaskChampionAdapter:
|
|
|
270
412
|
|
|
271
413
|
def done_task(self, task_id: TaskRef) -> None:
|
|
272
414
|
"""Mark a task as completed."""
|
|
415
|
+
self._locked_call(self._done_task_internal, task_id)
|
|
416
|
+
|
|
417
|
+
def _done_task_internal(self, task_id: TaskRef) -> None:
|
|
273
418
|
uuid_str = self._resolve_ref(task_id)
|
|
274
419
|
tc_task = self._get_tc_task(uuid_str)
|
|
275
420
|
ops = Operations()
|
|
@@ -280,6 +425,9 @@ class TaskChampionAdapter:
|
|
|
280
425
|
|
|
281
426
|
def start_task(self, task_id: TaskRef) -> None:
|
|
282
427
|
"""Start working on a task (set start timestamp)."""
|
|
428
|
+
self._locked_call(self._start_task_internal, task_id)
|
|
429
|
+
|
|
430
|
+
def _start_task_internal(self, task_id: TaskRef) -> None:
|
|
283
431
|
uuid_str = self._resolve_ref(task_id)
|
|
284
432
|
tc_task = self._get_tc_task(uuid_str)
|
|
285
433
|
ops = Operations()
|
|
@@ -289,6 +437,9 @@ class TaskChampionAdapter:
|
|
|
289
437
|
|
|
290
438
|
def stop_task(self, task_id: TaskRef) -> None:
|
|
291
439
|
"""Stop working on a task (clear start timestamp)."""
|
|
440
|
+
self._locked_call(self._stop_task_internal, task_id)
|
|
441
|
+
|
|
442
|
+
def _stop_task_internal(self, task_id: TaskRef) -> None:
|
|
292
443
|
uuid_str = self._resolve_ref(task_id)
|
|
293
444
|
tc_task = self._get_tc_task(uuid_str)
|
|
294
445
|
ops = Operations()
|
|
@@ -298,6 +449,9 @@ class TaskChampionAdapter:
|
|
|
298
449
|
|
|
299
450
|
def annotate_task(self, task_id: TaskRef, annotation: str) -> None:
|
|
300
451
|
"""Add an annotation to a task."""
|
|
452
|
+
self._locked_call(self._annotate_task_internal, task_id, annotation)
|
|
453
|
+
|
|
454
|
+
def _annotate_task_internal(self, task_id: TaskRef, annotation: str) -> None:
|
|
301
455
|
if not annotation or not annotation.strip():
|
|
302
456
|
raise TaskOperationError("Annotation text cannot be empty")
|
|
303
457
|
uuid_str = self._resolve_ref(task_id)
|
|
@@ -329,6 +483,9 @@ class TaskChampionAdapter:
|
|
|
329
483
|
Raises :exc:`~taskwarrior.exceptions.TaskSyncError` if no sync backend
|
|
330
484
|
is configured or if the sync operation fails.
|
|
331
485
|
"""
|
|
486
|
+
self._locked_call(self._synchronize_internal)
|
|
487
|
+
|
|
488
|
+
def _synchronize_internal(self) -> None:
|
|
332
489
|
if not self._sync_configured:
|
|
333
490
|
raise TaskSyncError(
|
|
334
491
|
"No sync server configured. "
|
|
@@ -369,6 +526,9 @@ class TaskChampionAdapter:
|
|
|
369
526
|
performing a sync. Call :meth:`synchronize` to pull remote
|
|
370
527
|
changes.
|
|
371
528
|
"""
|
|
529
|
+
return self._locked_call(self._has_local_changes_internal)
|
|
530
|
+
|
|
531
|
+
def _has_local_changes_internal(self) -> bool:
|
|
372
532
|
return self._replica.num_local_operations() > 0
|
|
373
533
|
|
|
374
534
|
def pending_local_ops_count(self) -> int:
|
|
@@ -377,6 +537,9 @@ class TaskChampionAdapter:
|
|
|
377
537
|
Useful for logging or debugging. ``0`` means fully synced (local side).
|
|
378
538
|
See :meth:`has_local_changes` for the boolean shorthand.
|
|
379
539
|
"""
|
|
540
|
+
return self._locked_call(self._pending_local_ops_count_internal)
|
|
541
|
+
|
|
542
|
+
def _pending_local_ops_count_internal(self) -> int:
|
|
380
543
|
return self._replica.num_local_operations()
|
|
381
544
|
|
|
382
545
|
# ------------------------------------------------------------------
|
|
@@ -438,6 +601,9 @@ class TaskChampionAdapter:
|
|
|
438
601
|
|
|
439
602
|
def get_projects(self) -> list[str]:
|
|
440
603
|
"""Return a sorted list of all project names across all tasks."""
|
|
604
|
+
return self._locked_call(self._get_projects_internal)
|
|
605
|
+
|
|
606
|
+
def _get_projects_internal(self) -> list[str]:
|
|
441
607
|
projects: set[str] = set()
|
|
442
608
|
for task in self._replica.all_tasks().values():
|
|
443
609
|
proj = task.get_value("project")
|
|
@@ -454,6 +620,9 @@ class TaskChampionAdapter:
|
|
|
454
620
|
When ``True``, TaskWarrior virtual tags (``TODAY``, ``READY``,
|
|
455
621
|
``OVERDUE``, …) are appended in addition to user-defined tags.
|
|
456
622
|
"""
|
|
623
|
+
return self._locked_call(self._get_tags_internal, include_virtual_tags)
|
|
624
|
+
|
|
625
|
+
def _get_tags_internal(self, include_virtual_tags: bool = False) -> list[str]:
|
|
457
626
|
tags: set[str] = set()
|
|
458
627
|
for task in self._replica.all_tasks().values():
|
|
459
628
|
for t in task.get_tags():
|
|
@@ -464,3 +633,11 @@ class TaskChampionAdapter:
|
|
|
464
633
|
|
|
465
634
|
tags.update(TASKWARRIOR_VIRTUAL_TAGS)
|
|
466
635
|
return sorted(tags)
|
|
636
|
+
|
|
637
|
+
def get_metrics(self) -> dict[str, Any]:
|
|
638
|
+
"""Return a snapshot of adapter operation metrics.
|
|
639
|
+
|
|
640
|
+
Returns a dict with ``calls_total``, ``errors_total``, and
|
|
641
|
+
``avg_wait_seconds`` — useful for monitoring lock contention.
|
|
642
|
+
"""
|
|
643
|
+
return self._metrics.snapshot()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/pytaskwarrior.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytaskwarrior-3.0.0a1 → pytaskwarrior-3.0.0a2}/src/taskwarrior/adapters/taskwarrior_adapter.py
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
|