megaplan-sdk 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.
- megaplan_sdk/__init__.py +67 -0
- megaplan_sdk/auth.py +185 -0
- megaplan_sdk/cache.py +192 -0
- megaplan_sdk/client.py +201 -0
- megaplan_sdk/constants.py +16 -0
- megaplan_sdk/exceptions.py +180 -0
- megaplan_sdk/helpers.py +108 -0
- megaplan_sdk/http_client.py +390 -0
- megaplan_sdk/logging_config.py +53 -0
- megaplan_sdk/models/__init__.py +22 -0
- megaplan_sdk/models/base.py +16 -0
- megaplan_sdk/models/comment.py +58 -0
- megaplan_sdk/models/common.py +107 -0
- megaplan_sdk/models/contractor.py +137 -0
- megaplan_sdk/models/deal.py +96 -0
- megaplan_sdk/models/department.py +40 -0
- megaplan_sdk/models/employee.py +117 -0
- megaplan_sdk/models/project.py +76 -0
- megaplan_sdk/models/task.py +75 -0
- megaplan_sdk/resources/__init__.py +15 -0
- megaplan_sdk/resources/auth.py +73 -0
- megaplan_sdk/resources/base.py +794 -0
- megaplan_sdk/resources/comments.py +148 -0
- megaplan_sdk/resources/contractors.py +173 -0
- megaplan_sdk/resources/deals.py +625 -0
- megaplan_sdk/resources/departments.py +70 -0
- megaplan_sdk/resources/employees.py +216 -0
- megaplan_sdk/resources/full_details.py +143 -0
- megaplan_sdk/resources/projects.py +854 -0
- megaplan_sdk/resources/tasks.py +932 -0
- megaplan_sdk/types.py +56 -0
- megaplan_sdk-0.1.0.dist-info/METADATA +1383 -0
- megaplan_sdk-0.1.0.dist-info/RECORD +36 -0
- megaplan_sdk-0.1.0.dist-info/WHEEL +5 -0
- megaplan_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- megaplan_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
"""Tasks resource for Megaplan API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from typing import Any, overload
|
|
7
|
+
|
|
8
|
+
from megaplan_sdk.constants import ContentType
|
|
9
|
+
from megaplan_sdk.models.comment import Comment
|
|
10
|
+
from megaplan_sdk.models.task import Task, TaskFullDetails
|
|
11
|
+
from megaplan_sdk.resources.base import BaseResource
|
|
12
|
+
from megaplan_sdk.resources.full_details import FullDetailsMixin, RelatedDataConfig
|
|
13
|
+
from megaplan_sdk.types import FilterType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TasksResource(BaseResource, FullDetailsMixin):
|
|
17
|
+
"""Resource for working with tasks."""
|
|
18
|
+
|
|
19
|
+
_full_details_config = [
|
|
20
|
+
RelatedDataConfig("sub_tasks", "include_sub_tasks", "get_sub_tasks"),
|
|
21
|
+
RelatedDataConfig("actual_sub_tasks", "include_actual_sub_tasks", "get_actual_sub_tasks"),
|
|
22
|
+
RelatedDataConfig(
|
|
23
|
+
"comments", "include_comments", "get_comments", limit_param="comments_limit"
|
|
24
|
+
),
|
|
25
|
+
RelatedDataConfig("history", "include_history", "get_history", limit_param="history_limit"),
|
|
26
|
+
RelatedDataConfig("auditors", "include_auditors", "get_auditors"),
|
|
27
|
+
RelatedDataConfig("executors", "include_executors", "get_executors"),
|
|
28
|
+
RelatedDataConfig("milestones", "include_milestones", "get_milestones"),
|
|
29
|
+
RelatedDataConfig(
|
|
30
|
+
"responsible_details",
|
|
31
|
+
"include_responsible_details",
|
|
32
|
+
None,
|
|
33
|
+
entity_field="responsible",
|
|
34
|
+
entity_type="employee",
|
|
35
|
+
),
|
|
36
|
+
RelatedDataConfig(
|
|
37
|
+
"owner_details",
|
|
38
|
+
"include_owner_details",
|
|
39
|
+
None,
|
|
40
|
+
entity_field="owner",
|
|
41
|
+
entity_type="employee",
|
|
42
|
+
),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
http_client,
|
|
48
|
+
cache=None,
|
|
49
|
+
default_comments_limit: int | None = None,
|
|
50
|
+
default_history_limit: int | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Initialize tasks resource.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
http_client: HTTP client for making requests.
|
|
56
|
+
cache: Optional entity cache.
|
|
57
|
+
default_comments_limit: Default limit for comments in get_full_details().
|
|
58
|
+
default_history_limit: Default limit for history in get_full_details().
|
|
59
|
+
"""
|
|
60
|
+
super().__init__(
|
|
61
|
+
http_client,
|
|
62
|
+
cache=cache,
|
|
63
|
+
default_comments_limit=default_comments_limit,
|
|
64
|
+
default_history_limit=default_history_limit,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def create(
|
|
68
|
+
self, task_data: dict[str, Any], auto_fill_required: bool = True
|
|
69
|
+
) -> Task:
|
|
70
|
+
"""Create a new task.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
task_data: Task data dictionary.
|
|
74
|
+
auto_fill_required: Automatically fill required fields if not provided.
|
|
75
|
+
Default: True. Sets isUrgent=False and isTemplate=False if not specified.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Created task.
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
>>> # Minimal task creation (auto-fills required fields)
|
|
82
|
+
>>> task = await client.tasks.create({"name": "New task"})
|
|
83
|
+
>>>
|
|
84
|
+
>>> # With explicit required fields
|
|
85
|
+
>>> task = await client.tasks.create({
|
|
86
|
+
... "name": "New task",
|
|
87
|
+
... "isUrgent": True,
|
|
88
|
+
... "isTemplate": False
|
|
89
|
+
... })
|
|
90
|
+
"""
|
|
91
|
+
# Auto-fill required fields if not provided
|
|
92
|
+
if auto_fill_required:
|
|
93
|
+
if "isUrgent" not in task_data:
|
|
94
|
+
task_data["isUrgent"] = False
|
|
95
|
+
if "isTemplate" not in task_data:
|
|
96
|
+
task_data["isTemplate"] = False
|
|
97
|
+
|
|
98
|
+
return await self._create_entity("task", task_data, Task)
|
|
99
|
+
|
|
100
|
+
@overload
|
|
101
|
+
async def list(
|
|
102
|
+
self,
|
|
103
|
+
*,
|
|
104
|
+
filter: FilterType | None = None,
|
|
105
|
+
statuses: list[str] | None = None,
|
|
106
|
+
limit: int | None = None,
|
|
107
|
+
page_after: dict[str, Any] | None = None,
|
|
108
|
+
page_before: dict[str, Any] | None = None,
|
|
109
|
+
page_with: dict[str, Any] | None = None,
|
|
110
|
+
fields: Any | None = None,
|
|
111
|
+
sort_by: list[dict[str, str]] | None = None,
|
|
112
|
+
only_requested_fields: bool | None = None,
|
|
113
|
+
expand: None = None,
|
|
114
|
+
) -> list[Task]: ...
|
|
115
|
+
|
|
116
|
+
@overload
|
|
117
|
+
async def list(
|
|
118
|
+
self,
|
|
119
|
+
*,
|
|
120
|
+
filter: FilterType | None = None,
|
|
121
|
+
statuses: list[str] | None = None,
|
|
122
|
+
limit: int | None = None,
|
|
123
|
+
page_after: dict[str, Any] | None = None,
|
|
124
|
+
page_before: dict[str, Any] | None = None,
|
|
125
|
+
page_with: dict[str, Any] | None = None,
|
|
126
|
+
fields: Any | None = None,
|
|
127
|
+
sort_by: list[dict[str, str]] | None = None,
|
|
128
|
+
only_requested_fields: bool | None = None,
|
|
129
|
+
expand: list[str],
|
|
130
|
+
) -> list[TaskFullDetails]: ...
|
|
131
|
+
|
|
132
|
+
async def list(
|
|
133
|
+
self,
|
|
134
|
+
filter: FilterType | None = None,
|
|
135
|
+
statuses: list[str] | None = None,
|
|
136
|
+
limit: int | None = None,
|
|
137
|
+
page_after: dict[str, Any] | None = None,
|
|
138
|
+
page_before: dict[str, Any] | None = None,
|
|
139
|
+
page_with: dict[str, Any] | None = None,
|
|
140
|
+
fields: Any | None = None,
|
|
141
|
+
sort_by: list[dict[str, str]] | None = None,
|
|
142
|
+
only_requested_fields: bool | None = None,
|
|
143
|
+
expand: list[str] | None = None,
|
|
144
|
+
) -> list[Task] | list[TaskFullDetails]:
|
|
145
|
+
"""Get list of tasks.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
filter: Task filter (ID or config).
|
|
149
|
+
statuses: List of statuses to filter by.
|
|
150
|
+
limit: Number of items per page.
|
|
151
|
+
page_after: Load page starting from this entity.
|
|
152
|
+
page_before: Load page strictly before this entity.
|
|
153
|
+
page_with: Load page containing this entity.
|
|
154
|
+
fields: Additional fields to include.
|
|
155
|
+
sort_by: Sort fields.
|
|
156
|
+
only_requested_fields: Return only requested fields.
|
|
157
|
+
expand: List of fields to expand (e.g., ["responsible", "owner"]).
|
|
158
|
+
Supported values: "responsible", "owner".
|
|
159
|
+
If provided, returns list[TaskFullDetails] instead of list[Task].
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
List of tasks (list[Task] if expand is None, list[TaskFullDetails] otherwise).
|
|
163
|
+
|
|
164
|
+
Examples:
|
|
165
|
+
>>> # Get tasks without expansion
|
|
166
|
+
>>> tasks = await client.tasks.list(limit=10)
|
|
167
|
+
>>>
|
|
168
|
+
>>> # Get tasks with expanded responsible and owner
|
|
169
|
+
>>> tasks_full = await client.tasks.list(
|
|
170
|
+
... limit=10, expand=["responsible", "owner"]
|
|
171
|
+
... )
|
|
172
|
+
>>> for task_full in tasks_full:
|
|
173
|
+
... if task_full.responsible_details:
|
|
174
|
+
... print(task_full.responsible_details.display_name())
|
|
175
|
+
"""
|
|
176
|
+
path = self._build_path("api", "v3", "task")
|
|
177
|
+
|
|
178
|
+
# Use base method to build params (DRY)
|
|
179
|
+
params = self._build_list_params(
|
|
180
|
+
filter=filter,
|
|
181
|
+
limit=limit,
|
|
182
|
+
page_after=page_after,
|
|
183
|
+
page_before=page_before,
|
|
184
|
+
page_with=page_with,
|
|
185
|
+
fields=fields,
|
|
186
|
+
sort_by=sort_by,
|
|
187
|
+
only_requested_fields=only_requested_fields,
|
|
188
|
+
statuses=statuses, # Extra param specific to tasks
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# 1. Fetch tasks
|
|
192
|
+
tasks = await self._get_list(path, Task, params)
|
|
193
|
+
|
|
194
|
+
# 2. If no expand, return as is
|
|
195
|
+
if not expand or not tasks:
|
|
196
|
+
return tasks
|
|
197
|
+
|
|
198
|
+
# 3. Batch load related entities
|
|
199
|
+
from megaplan_sdk.models.employee import Employee
|
|
200
|
+
|
|
201
|
+
expand_config: dict[str, tuple[str, type, str]] = {
|
|
202
|
+
"responsible": ("employee", Employee, ContentType.EMPLOYEE),
|
|
203
|
+
"owner": ("employee", Employee, ContentType.EMPLOYEE),
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
expanded = await self._expand_list_entities(tasks, expand, expand_config)
|
|
207
|
+
responsible_map = expanded.get("responsible", {})
|
|
208
|
+
owner_map = expanded.get("owner", {})
|
|
209
|
+
|
|
210
|
+
# 4. Build TaskFullDetails objects
|
|
211
|
+
results = []
|
|
212
|
+
for task in tasks:
|
|
213
|
+
resp_details = None
|
|
214
|
+
owner_details = None
|
|
215
|
+
|
|
216
|
+
if task.responsible and task.responsible.id in responsible_map:
|
|
217
|
+
resp_details = responsible_map[task.responsible.id]
|
|
218
|
+
|
|
219
|
+
if task.owner and task.owner.id in owner_map:
|
|
220
|
+
owner_details = owner_map[task.owner.id]
|
|
221
|
+
|
|
222
|
+
results.append(
|
|
223
|
+
TaskFullDetails(
|
|
224
|
+
task=task,
|
|
225
|
+
responsible_details=resp_details,
|
|
226
|
+
owner_details=owner_details,
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return results
|
|
231
|
+
|
|
232
|
+
async def get(self, task_id: int) -> Task:
|
|
233
|
+
"""Get task by ID.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
task_id: Task identifier.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Task details.
|
|
240
|
+
"""
|
|
241
|
+
return await self._get_entity("task", task_id, Task)
|
|
242
|
+
|
|
243
|
+
async def update(self, task_id: int, task_data: dict[str, Any]) -> Task:
|
|
244
|
+
"""Update task.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
task_id: Task identifier.
|
|
248
|
+
task_data: Updated task data.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Updated task.
|
|
252
|
+
"""
|
|
253
|
+
return await self._update_entity("task", task_id, task_data, Task)
|
|
254
|
+
|
|
255
|
+
async def delete(self, task_id: int) -> None:
|
|
256
|
+
"""Delete task.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
task_id: Task identifier.
|
|
260
|
+
"""
|
|
261
|
+
await self._delete_entity("task", task_id)
|
|
262
|
+
|
|
263
|
+
async def get_sub_tasks(
|
|
264
|
+
self,
|
|
265
|
+
task_id: int,
|
|
266
|
+
filters: list[dict[str, Any]] | None = None,
|
|
267
|
+
limit: int | None = None,
|
|
268
|
+
page_after: dict[str, Any] | None = None,
|
|
269
|
+
page_before: dict[str, Any] | None = None,
|
|
270
|
+
page_with: dict[str, Any] | None = None,
|
|
271
|
+
fields: Any | None = None,
|
|
272
|
+
sort_by: list[dict[str, str]] | None = None,
|
|
273
|
+
only_requested_fields: bool | None = None,
|
|
274
|
+
) -> list[Task]:
|
|
275
|
+
"""Get subtasks of a task.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
task_id: Task identifier.
|
|
279
|
+
filters: Task result type filters.
|
|
280
|
+
limit: Number of items per page.
|
|
281
|
+
page_after: Load page starting from this entity.
|
|
282
|
+
page_before: Load page strictly before this entity.
|
|
283
|
+
page_with: Load page containing this entity.
|
|
284
|
+
fields: Additional fields to include.
|
|
285
|
+
sort_by: Sort fields.
|
|
286
|
+
only_requested_fields: Return only requested fields.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
List of subtasks.
|
|
290
|
+
"""
|
|
291
|
+
path = self._build_path("api", "v3", "task", str(task_id), "subTasks")
|
|
292
|
+
|
|
293
|
+
params = self._build_list_params(
|
|
294
|
+
limit=limit,
|
|
295
|
+
page_after=page_after,
|
|
296
|
+
page_before=page_before,
|
|
297
|
+
page_with=page_with,
|
|
298
|
+
fields=fields,
|
|
299
|
+
sort_by=sort_by,
|
|
300
|
+
only_requested_fields=only_requested_fields,
|
|
301
|
+
filters=filters,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return await self._get_list(path, Task, params)
|
|
305
|
+
|
|
306
|
+
async def get_actual_sub_tasks(
|
|
307
|
+
self,
|
|
308
|
+
task_id: int,
|
|
309
|
+
filters: list[dict[str, Any]] | None = None,
|
|
310
|
+
limit: int | None = None,
|
|
311
|
+
page_after: dict[str, Any] | None = None,
|
|
312
|
+
page_before: dict[str, Any] | None = None,
|
|
313
|
+
page_with: dict[str, Any] | None = None,
|
|
314
|
+
fields: Any | None = None,
|
|
315
|
+
sort_by: list[dict[str, str]] | None = None,
|
|
316
|
+
only_requested_fields: bool | None = None,
|
|
317
|
+
) -> list[Task]:
|
|
318
|
+
"""Get actual subtasks of a task.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
task_id: Task identifier.
|
|
322
|
+
filters: Task result type filters.
|
|
323
|
+
limit: Number of items per page.
|
|
324
|
+
page_after: Load page starting from this entity.
|
|
325
|
+
page_before: Load page strictly before this entity.
|
|
326
|
+
page_with: Load page containing this entity.
|
|
327
|
+
fields: Additional fields to include.
|
|
328
|
+
sort_by: Sort fields.
|
|
329
|
+
only_requested_fields: Return only requested fields.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
List of actual subtasks.
|
|
333
|
+
"""
|
|
334
|
+
path = self._build_path("api", "v3", "task", str(task_id), "actualSubTasks")
|
|
335
|
+
|
|
336
|
+
params = self._build_list_params(
|
|
337
|
+
limit=limit,
|
|
338
|
+
page_after=page_after,
|
|
339
|
+
page_before=page_before,
|
|
340
|
+
page_with=page_with,
|
|
341
|
+
fields=fields,
|
|
342
|
+
sort_by=sort_by,
|
|
343
|
+
only_requested_fields=only_requested_fields,
|
|
344
|
+
filters=filters,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return await self._get_list(path, Task, params)
|
|
348
|
+
|
|
349
|
+
async def tree_level(
|
|
350
|
+
self,
|
|
351
|
+
filter: FilterType | None = None,
|
|
352
|
+
limit: int | None = None,
|
|
353
|
+
page_after: dict[str, Any] | None = None,
|
|
354
|
+
page_before: dict[str, Any] | None = None,
|
|
355
|
+
page_with: dict[str, Any] | None = None,
|
|
356
|
+
fields: Any | None = None,
|
|
357
|
+
sort_by: list[dict[str, str]] | None = None,
|
|
358
|
+
only_requested_fields: bool | None = None,
|
|
359
|
+
) -> list[Task]:
|
|
360
|
+
"""Get filtered list of projects or tasks at current tree level.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
filter: Task filter (ID or config).
|
|
364
|
+
limit: Number of items per page.
|
|
365
|
+
page_after: Load page starting from this entity.
|
|
366
|
+
page_before: Load page strictly before this entity.
|
|
367
|
+
page_with: Load page containing this entity.
|
|
368
|
+
fields: Additional fields to include.
|
|
369
|
+
sort_by: Sort fields.
|
|
370
|
+
only_requested_fields: Return only requested fields.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
List of tasks/projects at current tree level.
|
|
374
|
+
"""
|
|
375
|
+
path = self._build_path("api", "v3", "task", "treeLevel")
|
|
376
|
+
|
|
377
|
+
# Use base method to build params (DRY)
|
|
378
|
+
params = self._build_list_params(
|
|
379
|
+
filter=filter,
|
|
380
|
+
limit=limit,
|
|
381
|
+
page_after=page_after,
|
|
382
|
+
page_before=page_before,
|
|
383
|
+
page_with=page_with,
|
|
384
|
+
fields=fields,
|
|
385
|
+
sort_by=sort_by,
|
|
386
|
+
only_requested_fields=only_requested_fields,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
return await self._get_list(path, Task, params)
|
|
390
|
+
|
|
391
|
+
async def iterate(
|
|
392
|
+
self,
|
|
393
|
+
filter: FilterType | None = None,
|
|
394
|
+
statuses: list[str] | None = None,
|
|
395
|
+
limit: int = 100,
|
|
396
|
+
) -> AsyncIterator[Task]:
|
|
397
|
+
"""Iterate over all tasks with automatic pagination.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
filter: Task filter (ID or config).
|
|
401
|
+
statuses: List of statuses to filter by.
|
|
402
|
+
limit: Number of items per page.
|
|
403
|
+
|
|
404
|
+
Yields:
|
|
405
|
+
Task objects.
|
|
406
|
+
"""
|
|
407
|
+
task: Task
|
|
408
|
+
async for task in self._iterate_generic( # type: ignore[valid-type]
|
|
409
|
+
ContentType.TASK,
|
|
410
|
+
self.list,
|
|
411
|
+
limit,
|
|
412
|
+
filter=filter,
|
|
413
|
+
statuses=statuses,
|
|
414
|
+
):
|
|
415
|
+
yield task
|
|
416
|
+
|
|
417
|
+
async def get_comments(
|
|
418
|
+
self,
|
|
419
|
+
task_id: int,
|
|
420
|
+
limit: int | None = None,
|
|
421
|
+
page_after: dict[str, Any] | None = None,
|
|
422
|
+
page_before: dict[str, Any] | None = None,
|
|
423
|
+
page_with: dict[str, Any] | None = None,
|
|
424
|
+
) -> list[Comment]:
|
|
425
|
+
"""Get comments for a task.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
task_id: Task identifier.
|
|
429
|
+
limit: Number of items per page.
|
|
430
|
+
page_after: Load page starting from this entity.
|
|
431
|
+
page_before: Load page strictly before this entity.
|
|
432
|
+
page_with: Load page containing this entity.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
List of comments.
|
|
436
|
+
|
|
437
|
+
Examples:
|
|
438
|
+
>>> comments = await client.tasks.get_comments(task_id=123)
|
|
439
|
+
"""
|
|
440
|
+
return await self._get_entity_comments(
|
|
441
|
+
"task",
|
|
442
|
+
task_id,
|
|
443
|
+
limit,
|
|
444
|
+
page_after,
|
|
445
|
+
page_before,
|
|
446
|
+
page_with,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
async def create_comment(
|
|
450
|
+
self,
|
|
451
|
+
task_id: int,
|
|
452
|
+
text: str,
|
|
453
|
+
work: float | None = None,
|
|
454
|
+
attaches: list[dict[str, Any]] | None = None,
|
|
455
|
+
) -> Comment:
|
|
456
|
+
"""Create a comment for a task.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
task_id: Task identifier.
|
|
460
|
+
text: Comment text.
|
|
461
|
+
work: Hours worked (for time tracking).
|
|
462
|
+
attaches: List of file attachments.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Created comment.
|
|
466
|
+
|
|
467
|
+
Examples:
|
|
468
|
+
>>> comment = await client.tasks.create_comment(
|
|
469
|
+
... task_id=123,
|
|
470
|
+
... text="Work completed",
|
|
471
|
+
... work=2.5
|
|
472
|
+
... )
|
|
473
|
+
"""
|
|
474
|
+
extra_fields = {}
|
|
475
|
+
if work is not None:
|
|
476
|
+
# API expects workTime as DateInterval with seconds
|
|
477
|
+
# Convert hours to seconds
|
|
478
|
+
extra_fields["workTime"] = {
|
|
479
|
+
"contentType": "DateInterval",
|
|
480
|
+
"seconds": int(work * 3600), # Convert hours to seconds
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return await self._create_entity_comment(
|
|
484
|
+
"task",
|
|
485
|
+
task_id,
|
|
486
|
+
text,
|
|
487
|
+
attaches,
|
|
488
|
+
**extra_fields,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
async def create_simple(
|
|
492
|
+
self,
|
|
493
|
+
name: str,
|
|
494
|
+
responsible_id: int | None = None,
|
|
495
|
+
subject: str | None = None,
|
|
496
|
+
employees_resource: Any | None = None,
|
|
497
|
+
) -> Task:
|
|
498
|
+
"""Create a task with minimal required parameters.
|
|
499
|
+
|
|
500
|
+
Automatically fills required fields (isUrgent, isTemplate) and optionally
|
|
501
|
+
determines responsible from current user if not provided.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
name: Task name (required).
|
|
505
|
+
responsible_id: Responsible employee ID. If None and employees_resource
|
|
506
|
+
is provided, uses current user.
|
|
507
|
+
subject: Task description/subject.
|
|
508
|
+
employees_resource: EmployeesResource instance for auto-detecting current user.
|
|
509
|
+
If provided and responsible_id is None, will use current user as responsible.
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
Created task.
|
|
513
|
+
|
|
514
|
+
Examples:
|
|
515
|
+
>>> # Simple task with current user as responsible
|
|
516
|
+
>>> task = await client.tasks.create_simple(
|
|
517
|
+
... "New task",
|
|
518
|
+
... employees_resource=client.employees
|
|
519
|
+
... )
|
|
520
|
+
>>>
|
|
521
|
+
>>> # Simple task with specific responsible
|
|
522
|
+
>>> task = await client.tasks.create_simple(
|
|
523
|
+
... "New task",
|
|
524
|
+
... responsible_id=123
|
|
525
|
+
... )
|
|
526
|
+
"""
|
|
527
|
+
task_data: dict[str, Any] = {
|
|
528
|
+
"name": name,
|
|
529
|
+
"isUrgent": False,
|
|
530
|
+
"isTemplate": False,
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if subject:
|
|
534
|
+
task_data["subject"] = subject
|
|
535
|
+
|
|
536
|
+
# Auto-determine responsible from current user if not provided
|
|
537
|
+
if responsible_id is None and employees_resource:
|
|
538
|
+
try:
|
|
539
|
+
current_user = await employees_resource.get_current()
|
|
540
|
+
responsible_id = current_user.id
|
|
541
|
+
except Exception:
|
|
542
|
+
# If we can't get current user, skip responsible
|
|
543
|
+
# API will return error if required
|
|
544
|
+
pass
|
|
545
|
+
|
|
546
|
+
if responsible_id:
|
|
547
|
+
task_data["responsible"] = {"contentType": "Employee", "id": responsible_id}
|
|
548
|
+
|
|
549
|
+
return await self.create(task_data, auto_fill_required=False)
|
|
550
|
+
|
|
551
|
+
async def create_in_project(
|
|
552
|
+
self,
|
|
553
|
+
name: str,
|
|
554
|
+
project_id: int,
|
|
555
|
+
responsible_id: int | None = None,
|
|
556
|
+
subject: str | None = None,
|
|
557
|
+
employees_resource: Any | None = None,
|
|
558
|
+
) -> Task:
|
|
559
|
+
"""Create a task inside a project.
|
|
560
|
+
|
|
561
|
+
Automatically sets parent relationship to project and updates task after creation
|
|
562
|
+
to establish the link (as required by Megaplan API).
|
|
563
|
+
|
|
564
|
+
Args:
|
|
565
|
+
name: Task name (required).
|
|
566
|
+
project_id: Project ID to create task in.
|
|
567
|
+
responsible_id: Responsible employee ID. If None and employees_resource
|
|
568
|
+
is provided, uses current user.
|
|
569
|
+
subject: Task description/subject.
|
|
570
|
+
employees_resource: EmployeesResource instance for auto-detecting current user.
|
|
571
|
+
If provided and responsible_id is None, will use current user as responsible.
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
Created task (linked to project).
|
|
575
|
+
|
|
576
|
+
Examples:
|
|
577
|
+
>>> # Create task in project with current user as responsible
|
|
578
|
+
>>> task = await client.tasks.create_in_project(
|
|
579
|
+
... "Task in project",
|
|
580
|
+
... project_id=456,
|
|
581
|
+
... employees_resource=client.employees
|
|
582
|
+
... )
|
|
583
|
+
>>>
|
|
584
|
+
>>> # Create task in project with specific responsible
|
|
585
|
+
>>> task = await client.tasks.create_in_project(
|
|
586
|
+
... "Task in project",
|
|
587
|
+
... project_id=456,
|
|
588
|
+
... responsible_id=123
|
|
589
|
+
... )
|
|
590
|
+
"""
|
|
591
|
+
# Create task first
|
|
592
|
+
task = await self.create_simple(
|
|
593
|
+
name=name,
|
|
594
|
+
responsible_id=responsible_id,
|
|
595
|
+
subject=subject,
|
|
596
|
+
employees_resource=employees_resource,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# Update task to set parent relationship (required by API)
|
|
600
|
+
# Note: parent must be set via update, not create
|
|
601
|
+
update_data = {
|
|
602
|
+
"parent": {"contentType": "Project", "id": project_id},
|
|
603
|
+
}
|
|
604
|
+
task = await self.update(task.id, update_data)
|
|
605
|
+
|
|
606
|
+
return task
|
|
607
|
+
|
|
608
|
+
async def get_auditors(
|
|
609
|
+
self,
|
|
610
|
+
task_id: int,
|
|
611
|
+
limit: int | None = None,
|
|
612
|
+
page_after: dict[str, Any] | None = None,
|
|
613
|
+
page_before: dict[str, Any] | None = None,
|
|
614
|
+
page_with: dict[str, Any] | None = None,
|
|
615
|
+
) -> list[Any]:
|
|
616
|
+
"""Get auditors for a task.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
task_id: Task identifier.
|
|
620
|
+
limit: Number of items per page.
|
|
621
|
+
page_after: Load page starting from this entity.
|
|
622
|
+
page_before: Load page strictly before this entity.
|
|
623
|
+
page_with: Load page containing this entity.
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
List of auditors (Employees).
|
|
627
|
+
|
|
628
|
+
Examples:
|
|
629
|
+
>>> auditors = await client.tasks.get_auditors(task_id=123)
|
|
630
|
+
"""
|
|
631
|
+
return await self._get_entity_related_list(
|
|
632
|
+
"task", task_id, "auditors", limit, page_after, page_before, page_with
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
async def add_auditor(
|
|
636
|
+
self,
|
|
637
|
+
task_id: int,
|
|
638
|
+
auditor_id: int,
|
|
639
|
+
auditor_content_type: str = ContentType.EMPLOYEE,
|
|
640
|
+
) -> Any:
|
|
641
|
+
"""Add auditor to the task.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
task_id: Task identifier.
|
|
645
|
+
auditor_id: Auditor ID (usually Employee ID).
|
|
646
|
+
auditor_content_type: Content type (usually "Employee").
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
Added auditor.
|
|
650
|
+
|
|
651
|
+
Examples:
|
|
652
|
+
>>> auditor = await client.tasks.add_auditor(
|
|
653
|
+
... task_id=123,
|
|
654
|
+
... auditor_id=456
|
|
655
|
+
... )
|
|
656
|
+
"""
|
|
657
|
+
return await self._add_entity_related(
|
|
658
|
+
"task", task_id, "auditors", auditor_id, auditor_content_type
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
async def remove_auditor(
|
|
662
|
+
self,
|
|
663
|
+
task_id: int,
|
|
664
|
+
auditor_id: int,
|
|
665
|
+
auditor_content_type: str = ContentType.EMPLOYEE,
|
|
666
|
+
) -> None:
|
|
667
|
+
"""Remove auditor from the task.
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
task_id: Task identifier.
|
|
671
|
+
auditor_id: Auditor ID.
|
|
672
|
+
auditor_content_type: Content type (usually "Employee").
|
|
673
|
+
|
|
674
|
+
Examples:
|
|
675
|
+
>>> await client.tasks.remove_auditor(task_id=123, auditor_id=456)
|
|
676
|
+
"""
|
|
677
|
+
await self._remove_entity_related(
|
|
678
|
+
"task", task_id, "auditors", auditor_id, auditor_content_type
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
async def get_executors(
|
|
682
|
+
self,
|
|
683
|
+
task_id: int,
|
|
684
|
+
limit: int | None = None,
|
|
685
|
+
page_after: dict[str, Any] | None = None,
|
|
686
|
+
page_before: dict[str, Any] | None = None,
|
|
687
|
+
page_with: dict[str, Any] | None = None,
|
|
688
|
+
) -> list[Any]:
|
|
689
|
+
"""Get executors (co-performers) for a task.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
task_id: Task identifier.
|
|
693
|
+
limit: Number of items per page.
|
|
694
|
+
page_after: Load page starting from this entity.
|
|
695
|
+
page_before: Load page strictly before this entity.
|
|
696
|
+
page_with: Load page containing this entity.
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
List of executors (Employees).
|
|
700
|
+
|
|
701
|
+
Examples:
|
|
702
|
+
>>> executors = await client.tasks.get_executors(task_id=123)
|
|
703
|
+
"""
|
|
704
|
+
return await self._get_entity_related_list(
|
|
705
|
+
"task", task_id, "executors", limit, page_after, page_before, page_with
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
async def add_executor(
|
|
709
|
+
self,
|
|
710
|
+
task_id: int,
|
|
711
|
+
executor_id: int,
|
|
712
|
+
executor_content_type: str = ContentType.EMPLOYEE,
|
|
713
|
+
) -> Any:
|
|
714
|
+
"""Add executor (co-performer) to the task.
|
|
715
|
+
|
|
716
|
+
Args:
|
|
717
|
+
task_id: Task identifier.
|
|
718
|
+
executor_id: Executor ID (usually Employee ID).
|
|
719
|
+
executor_content_type: Content type (usually "Employee").
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
Added executor.
|
|
723
|
+
|
|
724
|
+
Examples:
|
|
725
|
+
>>> executor = await client.tasks.add_executor(
|
|
726
|
+
... task_id=123,
|
|
727
|
+
... executor_id=456
|
|
728
|
+
... )
|
|
729
|
+
"""
|
|
730
|
+
return await self._add_entity_related(
|
|
731
|
+
"task", task_id, "executors", executor_id, executor_content_type
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
async def remove_executor(
|
|
735
|
+
self,
|
|
736
|
+
task_id: int,
|
|
737
|
+
executor_id: int,
|
|
738
|
+
executor_content_type: str = ContentType.EMPLOYEE,
|
|
739
|
+
) -> None:
|
|
740
|
+
"""Remove executor from the task.
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
task_id: Task identifier.
|
|
744
|
+
executor_id: Executor ID.
|
|
745
|
+
executor_content_type: Content type (usually "Employee").
|
|
746
|
+
|
|
747
|
+
Examples:
|
|
748
|
+
>>> await client.tasks.remove_executor(task_id=123, executor_id=456)
|
|
749
|
+
"""
|
|
750
|
+
await self._remove_entity_related(
|
|
751
|
+
"task", task_id, "executors", executor_id, executor_content_type
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
async def get_milestones(
|
|
755
|
+
self,
|
|
756
|
+
task_id: int,
|
|
757
|
+
limit: int | None = None,
|
|
758
|
+
page_after: dict[str, Any] | None = None,
|
|
759
|
+
page_before: dict[str, Any] | None = None,
|
|
760
|
+
page_with: dict[str, Any] | None = None,
|
|
761
|
+
) -> list[Any]:
|
|
762
|
+
"""Get milestones for a task.
|
|
763
|
+
|
|
764
|
+
Args:
|
|
765
|
+
task_id: Task identifier.
|
|
766
|
+
limit: Number of items per page.
|
|
767
|
+
page_after: Load page starting from this entity.
|
|
768
|
+
page_before: Load page strictly before this entity.
|
|
769
|
+
page_with: Load page containing this entity.
|
|
770
|
+
|
|
771
|
+
Returns:
|
|
772
|
+
List of milestones.
|
|
773
|
+
|
|
774
|
+
Examples:
|
|
775
|
+
>>> milestones = await client.tasks.get_milestones(task_id=123)
|
|
776
|
+
"""
|
|
777
|
+
return await self._get_entity_related_list(
|
|
778
|
+
"task", task_id, "milestones", limit, page_after, page_before, page_with
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
async def add_milestone(
|
|
782
|
+
self,
|
|
783
|
+
task_id: int,
|
|
784
|
+
milestone_data: dict[str, Any],
|
|
785
|
+
) -> Any:
|
|
786
|
+
"""Add milestone to the task.
|
|
787
|
+
|
|
788
|
+
Args:
|
|
789
|
+
task_id: Task identifier.
|
|
790
|
+
milestone_data: Milestone data (name, date, etc.).
|
|
791
|
+
|
|
792
|
+
Returns:
|
|
793
|
+
Added milestone.
|
|
794
|
+
|
|
795
|
+
Examples:
|
|
796
|
+
>>> milestone = await client.tasks.add_milestone(
|
|
797
|
+
... task_id=123,
|
|
798
|
+
... milestone_data={"name": "Release 1.0", "date": "2026-02-01"}
|
|
799
|
+
... )
|
|
800
|
+
"""
|
|
801
|
+
return await self._add_entity_related(
|
|
802
|
+
"task", task_id, "milestones", 0, "Milestone", data_override=milestone_data
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
async def get_history(
|
|
806
|
+
self,
|
|
807
|
+
task_id: int,
|
|
808
|
+
limit: int | None = None,
|
|
809
|
+
page_after: dict[str, Any] | None = None,
|
|
810
|
+
page_before: dict[str, Any] | None = None,
|
|
811
|
+
page_with: dict[str, Any] | None = None,
|
|
812
|
+
) -> list[dict[str, Any]]:
|
|
813
|
+
"""Get history log for a task.
|
|
814
|
+
|
|
815
|
+
Args:
|
|
816
|
+
task_id: Task identifier.
|
|
817
|
+
limit: Number of items per page.
|
|
818
|
+
page_after: Load page starting from this entity.
|
|
819
|
+
page_before: Load page strictly before this entity.
|
|
820
|
+
page_with: Load page containing this entity.
|
|
821
|
+
|
|
822
|
+
Returns:
|
|
823
|
+
List of history entries.
|
|
824
|
+
|
|
825
|
+
Examples:
|
|
826
|
+
>>> history = await client.tasks.get_history(task_id=123, limit=10)
|
|
827
|
+
"""
|
|
828
|
+
return await self._get_entity_history(
|
|
829
|
+
"task", task_id, limit, page_after, page_before, page_with
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
async def search_history(
|
|
833
|
+
self,
|
|
834
|
+
task_id: int,
|
|
835
|
+
query: str,
|
|
836
|
+
limit: int | None = None,
|
|
837
|
+
page_after: dict[str, Any] | None = None,
|
|
838
|
+
page_before: dict[str, Any] | None = None,
|
|
839
|
+
page_with: dict[str, Any] | None = None,
|
|
840
|
+
) -> list[dict[str, Any]]:
|
|
841
|
+
"""Search in task history log.
|
|
842
|
+
|
|
843
|
+
Args:
|
|
844
|
+
task_id: Task identifier.
|
|
845
|
+
query: Search query.
|
|
846
|
+
limit: Number of items per page.
|
|
847
|
+
page_after: Load page starting from this entity.
|
|
848
|
+
page_before: Load page strictly before this entity.
|
|
849
|
+
page_with: Load page containing this entity.
|
|
850
|
+
|
|
851
|
+
Returns:
|
|
852
|
+
List of matching history entries.
|
|
853
|
+
|
|
854
|
+
Examples:
|
|
855
|
+
>>> results = await client.tasks.search_history(task_id=123, query="status")
|
|
856
|
+
"""
|
|
857
|
+
return await self._search_entity_history(
|
|
858
|
+
"task", task_id, query, limit, page_after, page_before, page_with
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
async def get_full_details(
|
|
862
|
+
self,
|
|
863
|
+
task_id: int,
|
|
864
|
+
include_sub_tasks: bool = False,
|
|
865
|
+
include_actual_sub_tasks: bool = False,
|
|
866
|
+
include_comments: bool = False,
|
|
867
|
+
include_history: bool = False,
|
|
868
|
+
include_auditors: bool = False,
|
|
869
|
+
include_executors: bool = False,
|
|
870
|
+
include_milestones: bool = False,
|
|
871
|
+
include_responsible_details: bool = False,
|
|
872
|
+
include_owner_details: bool = False,
|
|
873
|
+
comments_limit: int | None = None,
|
|
874
|
+
history_limit: int | None = None,
|
|
875
|
+
) -> TaskFullDetails:
|
|
876
|
+
"""Get full task details with related entities.
|
|
877
|
+
|
|
878
|
+
This method fetches the task and optionally loads related data in parallel
|
|
879
|
+
for better performance.
|
|
880
|
+
|
|
881
|
+
Args:
|
|
882
|
+
task_id: Task identifier.
|
|
883
|
+
include_sub_tasks: Load subtasks.
|
|
884
|
+
include_actual_sub_tasks: Load actual subtasks.
|
|
885
|
+
include_comments: Load task comments.
|
|
886
|
+
include_history: Load change history.
|
|
887
|
+
include_auditors: Load auditors list.
|
|
888
|
+
include_executors: Load executors/co-performers list.
|
|
889
|
+
include_milestones: Load milestones list.
|
|
890
|
+
include_responsible_details: Load full responsible (Employee) details.
|
|
891
|
+
include_owner_details: Load full owner (Employee) details.
|
|
892
|
+
comments_limit: Limit for comments (if included).
|
|
893
|
+
None = use global default (from MegaplanClient) or API default.
|
|
894
|
+
Explicit value overrides global default.
|
|
895
|
+
Example: comments_limit=50 returns max 50 comments.
|
|
896
|
+
history_limit: Limit for history (if included).
|
|
897
|
+
None = use global default (from MegaplanClient) or API default.
|
|
898
|
+
Explicit value overrides global default.
|
|
899
|
+
Example: history_limit=100 returns max 100 history entries.
|
|
900
|
+
|
|
901
|
+
Returns:
|
|
902
|
+
TaskFullDetails object with all requested data.
|
|
903
|
+
|
|
904
|
+
Examples:
|
|
905
|
+
>>> # Get task with subtasks and comments
|
|
906
|
+
>>> details = await client.tasks.get_full_details(
|
|
907
|
+
... task_id=123,
|
|
908
|
+
... include_sub_tasks=True,
|
|
909
|
+
... include_comments=True,
|
|
910
|
+
... include_responsible_details=True
|
|
911
|
+
... )
|
|
912
|
+
>>> print(details.task.name)
|
|
913
|
+
>>> print(details.responsible_details.first_name)
|
|
914
|
+
"""
|
|
915
|
+
return await self._get_full_details_generic(
|
|
916
|
+
entity_id=task_id,
|
|
917
|
+
entity_getter="get",
|
|
918
|
+
full_details_class=TaskFullDetails,
|
|
919
|
+
config=self._full_details_config,
|
|
920
|
+
main_entity_field="task",
|
|
921
|
+
include_sub_tasks=include_sub_tasks,
|
|
922
|
+
include_actual_sub_tasks=include_actual_sub_tasks,
|
|
923
|
+
include_comments=include_comments,
|
|
924
|
+
include_history=include_history,
|
|
925
|
+
include_auditors=include_auditors,
|
|
926
|
+
include_executors=include_executors,
|
|
927
|
+
include_milestones=include_milestones,
|
|
928
|
+
include_responsible_details=include_responsible_details,
|
|
929
|
+
include_owner_details=include_owner_details,
|
|
930
|
+
comments_limit=comments_limit,
|
|
931
|
+
history_limit=history_limit,
|
|
932
|
+
)
|