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