functualize-tasks 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,41 @@
1
+ """functualize-tasks — Tasks Domain SDK.
2
+
3
+ Provides the Tasks capability class, TaskProvider protocol, shared types,
4
+ errors, event constants, and testing doubles for task management.
5
+ """
6
+
7
+ from functualize_tasks._errors import TaskNotFoundError
8
+ from functualize_tasks._events import (
9
+ TASKS_COMPLETED,
10
+ TASKS_CREATED,
11
+ TASKS_DELETED,
12
+ TASKS_UPDATED,
13
+ )
14
+ from functualize_tasks._metadata import domain_metadata
15
+ from functualize_tasks._protocols import TaskProvider
16
+ from functualize_tasks._tasks import Tasks
17
+ from functualize_tasks._types import TaskItem, TaskLink, TaskStatus
18
+ from functualize_tasks.testing._mock_tasks import MockTaskOperation, MockTasks
19
+
20
+ __all__ = [
21
+ # Capability Class
22
+ "Tasks",
23
+ # Protocols
24
+ "TaskProvider",
25
+ # Types
26
+ "TaskItem",
27
+ "TaskLink",
28
+ "TaskStatus",
29
+ # Errors
30
+ "TaskNotFoundError",
31
+ # Event Constants
32
+ "TASKS_CREATED",
33
+ "TASKS_UPDATED",
34
+ "TASKS_COMPLETED",
35
+ "TASKS_DELETED",
36
+ # Testing Doubles
37
+ "MockTaskOperation",
38
+ "MockTasks",
39
+ # Metadata
40
+ "domain_metadata",
41
+ ]
@@ -0,0 +1,7 @@
1
+ """Error classes for the Tasks Domain SDK."""
2
+
3
+
4
+ class TaskNotFoundError(Exception):
5
+ """Raised when a task operation targets a task_id that does not exist."""
6
+
7
+ pass
@@ -0,0 +1,9 @@
1
+ """Event name constants for the Tasks Domain SDK.
2
+
3
+ Events follow the {domain}.{resource}.{action} grammar.
4
+ """
5
+
6
+ TASKS_CREATED: str = "tasks.task.created"
7
+ TASKS_UPDATED: str = "tasks.task.updated"
8
+ TASKS_COMPLETED: str = "tasks.task.completed"
9
+ TASKS_DELETED: str = "tasks.task.deleted"
@@ -0,0 +1,37 @@
1
+ """Domain metadata for the Tasks SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class DomainMetadata:
10
+ """Self-describing metadata for a domain SDK."""
11
+
12
+ name: str
13
+ display_name: str
14
+ description: str
15
+ capability_class: str
16
+ provider_protocol: str
17
+ config_section: str
18
+ entry_point_group: str
19
+ events_prefix: str
20
+ scaffold_template: str | None = None
21
+ documentation_url: str | None = None
22
+ mock_factory: str | None = None
23
+
24
+
25
+ domain_metadata = DomainMetadata(
26
+ name="tasks",
27
+ display_name="Tasks",
28
+ description="Task management and planning scratchpad",
29
+ capability_class="functualize_tasks.Tasks",
30
+ provider_protocol="functualize_tasks.TaskProvider",
31
+ config_section="tasks",
32
+ entry_point_group="functualize.tasks_providers",
33
+ events_prefix="tasks.",
34
+ scaffold_template=None,
35
+ documentation_url=None,
36
+ mock_factory="functualize_tasks.testing:MockTasks",
37
+ )
@@ -0,0 +1,89 @@
1
+ """Tasks domain protocol — TaskProvider.
2
+
3
+ Defines the protocol interface that task implementation plugins must satisfy.
4
+ The Tasks capability delegates all storage and retrieval operations to a
5
+ TaskProvider implementation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
11
+
12
+ if TYPE_CHECKING:
13
+ from functualize_tasks._types import TaskItem, TaskLink, TaskStatus
14
+
15
+
16
+ @runtime_checkable
17
+ class TaskProvider(Protocol):
18
+ """Protocol for task storage implementation plugins.
19
+
20
+ Implementation plugins (e.g., local state-backed, remote service) must
21
+ satisfy this protocol. The Tasks capability delegates all CRUD operations
22
+ to the active TaskProvider.
23
+ """
24
+
25
+ def add(self, title: str, linked_to: TaskLink | None = None) -> str:
26
+ """Create a new task and return its generated unique ID.
27
+
28
+ Args:
29
+ title: Human-readable title for the task.
30
+ linked_to: Optional link associating the task with a job,
31
+ workflow step, or job phase.
32
+
33
+ Returns:
34
+ The unique identifier of the newly created task.
35
+ """
36
+ ...
37
+
38
+ def list(
39
+ self, status: TaskStatus | None = None, filter: str | None = None
40
+ ) -> list[TaskItem]:
41
+ """List tasks, optionally filtered by status or title substring.
42
+
43
+ Args:
44
+ status: If provided, return only tasks matching this status.
45
+ filter: If provided, return only tasks whose title contains
46
+ this substring.
47
+
48
+ Returns:
49
+ A list of matching TaskItem instances.
50
+ """
51
+ ...
52
+
53
+ def update(
54
+ self, task_id: str, status: TaskStatus | None = None, notes: str | None = None
55
+ ) -> None:
56
+ """Update a task's status and/or notes.
57
+
58
+ Args:
59
+ task_id: The unique identifier of the task to update.
60
+ status: If provided, the new status to set.
61
+ notes: If provided, the new notes to set.
62
+
63
+ Raises:
64
+ TaskNotFoundError: If the task_id does not exist.
65
+ """
66
+ ...
67
+
68
+ def delete(self, task_id: str) -> None:
69
+ """Delete a task by its ID.
70
+
71
+ Args:
72
+ task_id: The unique identifier of the task to delete.
73
+
74
+ Raises:
75
+ TaskNotFoundError: If the task_id does not exist.
76
+ """
77
+ ...
78
+
79
+ def link(self, task_id: str, linked_to: TaskLink) -> None:
80
+ """Associate a task with a job, workflow step, or job phase.
81
+
82
+ Args:
83
+ task_id: The unique identifier of the task to link.
84
+ linked_to: The link specifying the kind and target.
85
+
86
+ Raises:
87
+ TaskNotFoundError: If the task_id does not exist.
88
+ """
89
+ ...
@@ -0,0 +1,250 @@
1
+ """Tasks capability class — mutable planning scratchpad.
2
+
3
+ The Tasks class provides methods to add, list, update, delete, and link
4
+ tasks. It delegates all storage operations to a TaskProvider and emits
5
+ structured events via a duck-typed EventBus on every mutation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import uuid
11
+ from typing import TYPE_CHECKING, Any, Protocol
12
+
13
+ from functualize_tasks._errors import TaskNotFoundError
14
+ from functualize_tasks._events import (
15
+ TASKS_COMPLETED,
16
+ TASKS_CREATED,
17
+ TASKS_DELETED,
18
+ TASKS_UPDATED,
19
+ )
20
+ from functualize_tasks._types import TaskItem, TaskLink, TaskStatus
21
+
22
+ if TYPE_CHECKING:
23
+ from functualize_tasks._protocols import TaskProvider
24
+
25
+
26
+ class _EventBus(Protocol):
27
+ """Duck-typed EventBus — only requires an emit method."""
28
+
29
+ def emit(self, event_name: str, **payload: Any) -> None: ...
30
+
31
+
32
+ class _InMemoryTaskProvider:
33
+ """Simple in-memory TaskProvider used when no external provider is configured.
34
+
35
+ Stores tasks in a dict keyed by task ID. Suitable for ephemeral use
36
+ when no persistent TaskProvider plugin is installed.
37
+ """
38
+
39
+ def __init__(self) -> None:
40
+ self._tasks: dict[str, TaskItem] = {}
41
+
42
+ def add(self, title: str, linked_to: TaskLink | None = None) -> str:
43
+ task_id = uuid.uuid4().hex
44
+ task = TaskItem(
45
+ id=task_id,
46
+ title=title,
47
+ status=TaskStatus.PENDING,
48
+ linked_to=linked_to,
49
+ )
50
+ self._tasks[task_id] = task
51
+ return task_id
52
+
53
+ def list(
54
+ self, status: TaskStatus | None = None, filter: str | None = None
55
+ ) -> list[TaskItem]:
56
+ results = list(self._tasks.values())
57
+ if status is not None:
58
+ results = [t for t in results if t.status == status]
59
+ if filter is not None:
60
+ results = [t for t in results if filter in t.title]
61
+ return results
62
+
63
+ def get(self, task_id: str) -> TaskItem | None:
64
+ return self._tasks.get(task_id)
65
+
66
+ def update(
67
+ self, task_id: str, status: TaskStatus | None = None, notes: str | None = None
68
+ ) -> None:
69
+ task = self._tasks.get(task_id)
70
+ if task is None:
71
+ raise TaskNotFoundError(f"Task '{task_id}' not found.")
72
+ # Build updated task (frozen dataclass — must reconstruct)
73
+ updates: dict[str, Any] = {}
74
+ if status is not None:
75
+ updates["status"] = status
76
+ if notes is not None:
77
+ updates["notes"] = notes
78
+ if updates:
79
+ from dataclasses import asdict
80
+
81
+ data = asdict(task)
82
+ data.update(updates)
83
+ self._tasks[task_id] = TaskItem(**data)
84
+
85
+ def delete(self, task_id: str) -> None:
86
+ if task_id not in self._tasks:
87
+ raise TaskNotFoundError(f"Task '{task_id}' not found.")
88
+ del self._tasks[task_id]
89
+
90
+ def link(self, task_id: str, linked_to: TaskLink) -> None:
91
+ task = self._tasks.get(task_id)
92
+ if task is None:
93
+ raise TaskNotFoundError(f"Task '{task_id}' not found.")
94
+ from dataclasses import asdict
95
+
96
+ data = asdict(task)
97
+ data["linked_to"] = linked_to
98
+ self._tasks[task_id] = TaskItem(**data)
99
+
100
+
101
+ class Tasks:
102
+ """Task management capability — mutable planning scratchpad.
103
+
104
+ Provides methods to create, list, update, delete, and link tasks.
105
+ Delegates all storage to a TaskProvider implementation and emits
106
+ structured events on every mutation via a duck-typed EventBus.
107
+
108
+ Args:
109
+ _provider: The TaskProvider implementation for persistence.
110
+ If None, an in-memory provider is used.
111
+ _event_bus: Optional duck-typed EventBus with an emit(event_name, **payload)
112
+ method. If None, events are silently discarded.
113
+ """
114
+
115
+ def __init__(
116
+ self,
117
+ *,
118
+ _provider: TaskProvider | None = None,
119
+ _event_bus: _EventBus | None = None,
120
+ ) -> None:
121
+ self._provider: TaskProvider = (
122
+ _provider if _provider is not None else _InMemoryTaskProvider()
123
+ ) # type: ignore[assignment]
124
+ self._event_bus = _event_bus
125
+
126
+ def _emit(self, event_name: str, **payload: Any) -> None:
127
+ """Emit an event if an event bus is available."""
128
+ if self._event_bus is not None:
129
+ self._event_bus.emit(event_name, **payload)
130
+
131
+ def add(self, title: str, *, linked_to: TaskLink | None = None) -> str:
132
+ """Create a new task and return its generated unique ID.
133
+
134
+ Emits a ``tasks.task.created`` event with payload containing the
135
+ task id, title, and linked_to.
136
+
137
+ Args:
138
+ title: Human-readable title for the task.
139
+ linked_to: Optional link associating the task with a job,
140
+ workflow step, or job phase.
141
+
142
+ Returns:
143
+ The unique identifier of the newly created task.
144
+ """
145
+ task_id = self._provider.add(title, linked_to)
146
+ self._emit(
147
+ TASKS_CREATED,
148
+ task_id=task_id,
149
+ title=title,
150
+ linked_to=linked_to,
151
+ )
152
+ return task_id
153
+
154
+ def list(
155
+ self,
156
+ *,
157
+ status: TaskStatus | None = None,
158
+ filter: str | None = None,
159
+ ) -> list[TaskItem]:
160
+ """List tasks, optionally filtered by status or title substring.
161
+
162
+ Args:
163
+ status: If provided, return only tasks matching this status.
164
+ filter: If provided, return only tasks whose title contains
165
+ this substring.
166
+
167
+ Returns:
168
+ A list of matching TaskItem instances.
169
+ """
170
+ return self._provider.list(status, filter)
171
+
172
+ def update(
173
+ self,
174
+ task_id: str,
175
+ *,
176
+ status: TaskStatus | None = None,
177
+ notes: str | None = None,
178
+ ) -> None:
179
+ """Update a task's status and/or notes.
180
+
181
+ Emits ``tasks.task.updated`` when the status changes, and
182
+ ``tasks.task.completed`` when the new status is DONE.
183
+
184
+ Args:
185
+ task_id: The unique identifier of the task to update.
186
+ status: If provided, the new status to set.
187
+ notes: If provided, the new notes to set.
188
+
189
+ Raises:
190
+ TaskNotFoundError: If the task_id does not exist.
191
+ """
192
+ # Retrieve old status before updating (for event payload)
193
+ old_tasks = self._provider.list()
194
+ old_task = next((t for t in old_tasks if t.id == task_id), None)
195
+ if old_task is None:
196
+ raise TaskNotFoundError(f"Task '{task_id}' not found.")
197
+
198
+ old_status = old_task.status
199
+
200
+ self._provider.update(task_id, status, notes)
201
+
202
+ # Emit updated event when status changes
203
+ if status is not None and status != old_status:
204
+ self._emit(
205
+ TASKS_UPDATED,
206
+ task_id=task_id,
207
+ old_status=old_status.value,
208
+ new_status=status.value,
209
+ )
210
+ # Emit completed event when transitioning to DONE
211
+ if status == TaskStatus.DONE:
212
+ self._emit(TASKS_COMPLETED, task_id=task_id)
213
+
214
+ def delete(self, task_id: str) -> None:
215
+ """Delete a task by its ID.
216
+
217
+ Emits a ``tasks.task.deleted`` event with the task id.
218
+
219
+ Args:
220
+ task_id: The unique identifier of the task to delete.
221
+
222
+ Raises:
223
+ TaskNotFoundError: If the task_id does not exist.
224
+ """
225
+ # Verify existence before delegating (requirement: raise immediately)
226
+ old_tasks = self._provider.list()
227
+ old_task = next((t for t in old_tasks if t.id == task_id), None)
228
+ if old_task is None:
229
+ raise TaskNotFoundError(f"Task '{task_id}' not found.")
230
+
231
+ self._provider.delete(task_id)
232
+ self._emit(TASKS_DELETED, task_id=task_id)
233
+
234
+ def link(self, task_id: str, linked_to: TaskLink) -> None:
235
+ """Associate a task with a job, workflow step, or job phase.
236
+
237
+ Args:
238
+ task_id: The unique identifier of the task to link.
239
+ linked_to: The link specifying the kind and target.
240
+
241
+ Raises:
242
+ TaskNotFoundError: If the task_id does not exist.
243
+ """
244
+ # Verify existence before delegating
245
+ old_tasks = self._provider.list()
246
+ old_task = next((t for t in old_tasks if t.id == task_id), None)
247
+ if old_task is None:
248
+ raise TaskNotFoundError(f"Task '{task_id}' not found.")
249
+
250
+ self._provider.link(task_id, linked_to)
@@ -0,0 +1,52 @@
1
+ """Shared types for the Tasks Domain SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import StrEnum
7
+
8
+
9
+ class TaskStatus(StrEnum):
10
+ """Status of a task item."""
11
+
12
+ PENDING = "pending"
13
+ IN_PROGRESS = "in_progress"
14
+ DONE = "done"
15
+ SKIPPED = "skipped"
16
+ BLOCKED = "blocked"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class TaskLink:
21
+ """Specifies what a task is optionally linked to.
22
+
23
+ Attributes:
24
+ kind: The type of link — "job", "workflow_step", or "job_phase".
25
+ target: The identifier of the linked entity.
26
+ """
27
+
28
+ kind: str # "job" | "workflow_step" | "job_phase"
29
+ target: str
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class TaskItem:
34
+ """A single task with id, title, status, and optional metadata.
35
+
36
+ Attributes:
37
+ id: Unique identifier of the task.
38
+ title: Human-readable title of the task.
39
+ status: Current status of the task.
40
+ linked_to: Optional link to a job, workflow step, or job phase.
41
+ notes: Optional free-form notes.
42
+ creator: Optional identifier of who created the task.
43
+ created_at: Optional UNIX timestamp of creation time.
44
+ """
45
+
46
+ id: str
47
+ title: str
48
+ status: TaskStatus
49
+ linked_to: TaskLink | None = None
50
+ notes: str | None = None
51
+ creator: str | None = None
52
+ created_at: float | None = None
File without changes
@@ -0,0 +1,13 @@
1
+ """Tasks testing doubles — MockTasks.
2
+
3
+ Provides a deterministic, operation-capturing testing double for the Tasks
4
+ capability, suitable for unit and integration testing of jobs that use
5
+ task management features.
6
+ """
7
+
8
+ from functualize_tasks.testing._mock_tasks import MockTaskOperation, MockTasks
9
+
10
+ __all__ = [
11
+ "MockTaskOperation",
12
+ "MockTasks",
13
+ ]
@@ -0,0 +1,168 @@
1
+ """MockTasks — operation-capturing testing double.
2
+
3
+ Provides a Tasks implementation for testing that captures all operations
4
+ (add, list, update, delete, link) for assertion. Backed by an in-memory
5
+ TaskProvider so operations actually execute, and all calls are recorded
6
+ in a queryable log.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from functualize_tasks._tasks import Tasks, _InMemoryTaskProvider
15
+
16
+ if TYPE_CHECKING:
17
+ from functualize_tasks._types import TaskItem, TaskLink, TaskStatus
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class MockTaskOperation:
22
+ """A recorded operation on the MockTasks instance.
23
+
24
+ Attributes:
25
+ method: The method name that was called (e.g., "add", "list").
26
+ args: Positional arguments as a tuple.
27
+ kwargs: Keyword arguments as a dict.
28
+ result: The return value of the operation (None for void methods).
29
+ """
30
+
31
+ method: str
32
+ args: tuple[Any, ...] = ()
33
+ kwargs: dict[str, Any] = field(default_factory=dict)
34
+ result: Any = None
35
+
36
+
37
+ class MockTasks(Tasks):
38
+ """Testing double for Tasks that captures all operations for assertion.
39
+
40
+ Extends the Tasks capability with a real in-memory provider so that
41
+ operations actually execute (add creates tasks, list returns them, etc.),
42
+ while also recording every call in an operations log that tests can
43
+ query and assert against.
44
+
45
+ Example:
46
+ >>> tasks = MockTasks()
47
+ >>> task_id = tasks.add("Write tests")
48
+ >>> tasks.update(task_id, status=TaskStatus.IN_PROGRESS)
49
+ >>> assert len(tasks.operations) == 2
50
+ >>> assert tasks.operations[0].method == "add"
51
+ >>> assert tasks.operations[1].method == "update"
52
+ >>> assert tasks.adds == [tasks.operations[0]]
53
+
54
+ Args:
55
+ None — uses an internal in-memory provider automatically.
56
+ """
57
+
58
+ def __init__(self) -> None:
59
+ provider = _InMemoryTaskProvider()
60
+ super().__init__(_provider=provider)
61
+ self._operations: list[MockTaskOperation] = []
62
+
63
+ @property
64
+ def operations(self) -> list[MockTaskOperation]:
65
+ """All recorded operations in call order."""
66
+ return list(self._operations)
67
+
68
+ @property
69
+ def adds(self) -> list[MockTaskOperation]:
70
+ """All recorded 'add' operations."""
71
+ return [op for op in self._operations if op.method == "add"]
72
+
73
+ @property
74
+ def lists(self) -> list[MockTaskOperation]:
75
+ """All recorded 'list' operations."""
76
+ return [op for op in self._operations if op.method == "list"]
77
+
78
+ @property
79
+ def updates(self) -> list[MockTaskOperation]:
80
+ """All recorded 'update' operations."""
81
+ return [op for op in self._operations if op.method == "update"]
82
+
83
+ @property
84
+ def deletes(self) -> list[MockTaskOperation]:
85
+ """All recorded 'delete' operations."""
86
+ return [op for op in self._operations if op.method == "delete"]
87
+
88
+ @property
89
+ def links(self) -> list[MockTaskOperation]:
90
+ """All recorded 'link' operations."""
91
+ return [op for op in self._operations if op.method == "link"]
92
+
93
+ def reset(self) -> None:
94
+ """Clear all recorded operations."""
95
+ self._operations.clear()
96
+
97
+ def add(self, title: str, *, linked_to: TaskLink | None = None) -> str:
98
+ """Create a task and record the operation."""
99
+ result = super().add(title, linked_to=linked_to)
100
+ self._operations.append(
101
+ MockTaskOperation(
102
+ method="add",
103
+ args=(title,),
104
+ kwargs={"linked_to": linked_to},
105
+ result=result,
106
+ )
107
+ )
108
+ return result
109
+
110
+ def list(
111
+ self,
112
+ *,
113
+ status: TaskStatus | None = None,
114
+ filter: str | None = None,
115
+ ) -> list[TaskItem]:
116
+ """List tasks and record the operation."""
117
+ result = super().list(status=status, filter=filter)
118
+ self._operations.append(
119
+ MockTaskOperation(
120
+ method="list",
121
+ args=(),
122
+ kwargs={"status": status, "filter": filter},
123
+ result=result,
124
+ )
125
+ )
126
+ return result
127
+
128
+ def update(
129
+ self,
130
+ task_id: str,
131
+ *,
132
+ status: TaskStatus | None = None,
133
+ notes: str | None = None,
134
+ ) -> None:
135
+ """Update a task and record the operation."""
136
+ super().update(task_id, status=status, notes=notes)
137
+ self._operations.append(
138
+ MockTaskOperation(
139
+ method="update",
140
+ args=(task_id,),
141
+ kwargs={"status": status, "notes": notes},
142
+ result=None,
143
+ )
144
+ )
145
+
146
+ def delete(self, task_id: str) -> None:
147
+ """Delete a task and record the operation."""
148
+ super().delete(task_id)
149
+ self._operations.append(
150
+ MockTaskOperation(
151
+ method="delete",
152
+ args=(task_id,),
153
+ kwargs={},
154
+ result=None,
155
+ )
156
+ )
157
+
158
+ def link(self, task_id: str, linked_to: TaskLink) -> None:
159
+ """Link a task and record the operation."""
160
+ super().link(task_id, linked_to=linked_to)
161
+ self._operations.append(
162
+ MockTaskOperation(
163
+ method="link",
164
+ args=(task_id,),
165
+ kwargs={"linked_to": linked_to},
166
+ result=None,
167
+ )
168
+ )
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: functualize-tasks
3
+ Version: 0.1.0
4
+ Summary: Tasks Domain SDK for functualize — task management capabilities
5
+ Author-email: Mohammad Hakim Adiprasetya <viltohmyst@gmail.com>
6
+ License-Expression: MIT
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: pydantic>=2.0.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
17
+ Requires-Dist: pytest>=7.4.0; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # functualize-tasks
21
+
22
+ > **Status: Published** — Independently installable from PyPI.
23
+
24
+ Tasks Domain SDK for the functualize framework. Provides a task management capability
25
+ that acts as a mutable planning scratchpad within job execution — create, list, update,
26
+ delete, and link tasks with structured event emission on every mutation. Storage is
27
+ delegated to pluggable `TaskProvider` implementations, making the capability
28
+ backend-agnostic.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install functualize-tasks
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ```python
39
+ from functualize_tasks import Tasks, TaskStatus, TaskLink
40
+
41
+ tasks = Tasks()
42
+
43
+ # Create tasks
44
+ task_id = tasks.add("Run database migration")
45
+ tasks.add("Deploy service", linked_to=TaskLink(kind="job", target="deploy"))
46
+
47
+ # Update status
48
+ tasks.update(task_id, status=TaskStatus.IN_PROGRESS)
49
+ tasks.update(task_id, status=TaskStatus.DONE, notes="Migration complete")
50
+
51
+ # List and filter
52
+ pending = tasks.list(status=TaskStatus.PENDING)
53
+ all_tasks = tasks.list(filter="service")
54
+ ```
55
+
56
+ ## Features
57
+
58
+ - **CRUD task management** — add, list, update, delete, and link tasks with a clean method-call API
59
+ - **Event-driven mutations** — every state change emits structured events (`tasks.task.created`, `tasks.task.updated`, `tasks.task.completed`, `tasks.task.deleted`) via a duck-typed EventBus
60
+ - **Pluggable storage** — implement the `TaskProvider` protocol to back tasks with any persistence layer (in-memory, SQLite, remote service)
61
+ - **Rich data model** — `TaskItem` with status enum (`PENDING`, `IN_PROGRESS`, `DONE`, `SKIPPED`, `BLOCKED`), optional `TaskLink` to associate tasks with jobs or workflow steps
62
+ - **Testing double included** — `MockTasks` captures all operations for assertion while executing against a real in-memory provider
63
+ - **Type-safe** — fully typed with PEP 561 `py.typed` marker for mypy/pyright support
64
+
65
+ ## API Reference
66
+
67
+ Public classes, types, and constants exported by this plugin:
68
+
69
+ ### Capability
70
+
71
+ - `Tasks` — Main capability class providing `add()`, `list()`, `update()`, `delete()`, and `link()` methods
72
+
73
+ ### Protocols
74
+
75
+ - `TaskProvider` — Runtime-checkable protocol that storage backends must implement
76
+
77
+ ### Types
78
+
79
+ - `TaskItem` — Frozen dataclass representing a task (id, title, status, linked_to, notes, creator, created_at)
80
+ - `TaskLink` — Frozen dataclass specifying a link kind and target
81
+ - `TaskStatus` — String enum with values: `PENDING`, `IN_PROGRESS`, `DONE`, `SKIPPED`, `BLOCKED`
82
+
83
+ ### Errors
84
+
85
+ - `TaskNotFound` — Raised when an operation targets a non-existent task ID
86
+
87
+ ### Event Constants
88
+
89
+ - `TASKS_CREATED` — `"tasks.task.created"`
90
+ - `TASKS_UPDATED` — `"tasks.task.updated"`
91
+ - `TASKS_COMPLETED` — `"tasks.task.completed"`
92
+ - `TASKS_DELETED` — `"tasks.task.deleted"`
93
+
94
+ ### Testing
95
+
96
+ - `MockTasks` — Operation-capturing testing double with `operations`, `adds`, `updates`, `deletes`, `links` properties
97
+ - `MockTaskOperation` — Frozen dataclass recording a single captured method call (method, args, kwargs, result)
98
+
99
+ ### Metadata
100
+
101
+ - `domain_metadata` — `DomainMetadata` instance describing the tasks domain SDK
102
+
103
+ ## Development
104
+
105
+ Run plugin tests:
106
+
107
+ ```bash
108
+ uv run pytest plugins/functualize-tasks/tests/ -v
109
+ ```
@@ -0,0 +1,14 @@
1
+ functualize_tasks/__init__.py,sha256=eM-ngF-vtVP16O9fiZbLXTnTCRrzL-97LZTH88cby7A,1062
2
+ functualize_tasks/_errors.py,sha256=HesVnN5wyNSallDcEeGQt2hVNdpmMzkfLr3FUVnKTrk,172
3
+ functualize_tasks/_events.py,sha256=LJqbnfwCBUmiCfUa5yC2ieEP479t3zDfhGgYUOscgf0,284
4
+ functualize_tasks/_metadata.py,sha256=_lGrQZ2zr5Qj_lygmaRYdNDXflSSxHmCZ-JA1YZVRPI,982
5
+ functualize_tasks/_protocols.py,sha256=-odBf9i2g5EEJQo9RIYQ3h8c4bEAn0SVcAFPxWlXnqo,2734
6
+ functualize_tasks/_tasks.py,sha256=XzZTIwBCDM3imb6fds0i29eB3_dqbV2d2NKkJfOJAmQ,8446
7
+ functualize_tasks/_types.py,sha256=U8KF1_tlk2-GzThRy9HpF5hxaA5xYP4JOvtP2HtGo1M,1340
8
+ functualize_tasks/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ functualize_tasks/testing/__init__.py,sha256=74ZpwLS0x3Ka9If8n6Say2QvVKCFy8rb0GKlZ2dVhPI,354
10
+ functualize_tasks/testing/_mock_tasks.py,sha256=sjq_R4b9mx7114RB9wPcg_FIMOzXOkyzM09Dp5iAW2U,5426
11
+ functualize_tasks-0.1.0.dist-info/METADATA,sha256=gFQV4xcOQwb6lUUAyzl-EFFV3mC50OGSBCv41AkK9LA,3900
12
+ functualize_tasks-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
13
+ functualize_tasks-0.1.0.dist-info/entry_points.txt,sha256=cbWHKEshIClHGASgLoLdtL64Etibr2-_QcUjqCXuZwc,74
14
+ functualize_tasks-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [functualize.domains]
2
+ tasks = functualize_tasks._metadata:domain_metadata