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.
@@ -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
+ )