megaplan-sdk 0.2.2__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {megaplan_sdk-0.2.2/src/megaplan_sdk.egg-info → megaplan_sdk-0.2.3}/PKG-INFO +84 -2
  2. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/README.md +83 -1
  3. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/pyproject.toml +1 -1
  4. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/__init__.py +4 -1
  5. megaplan_sdk-0.2.3/src/megaplan_sdk/constants.py +42 -0
  6. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/models/task.py +7 -0
  7. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/resources/comments.py +61 -4
  8. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/resources/deals.py +7 -0
  9. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/resources/full_details.py +15 -0
  10. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/resources/projects.py +7 -0
  11. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/resources/tasks.py +27 -1
  12. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3/src/megaplan_sdk.egg-info}/PKG-INFO +84 -2
  13. megaplan_sdk-0.2.2/src/megaplan_sdk/constants.py +0 -17
  14. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/LICENSE +0 -0
  15. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/setup.cfg +0 -0
  16. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/auth.py +0 -0
  17. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/cache.py +0 -0
  18. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/client.py +0 -0
  19. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/exceptions.py +0 -0
  20. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/filter_builder.py +0 -0
  21. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/helpers.py +0 -0
  22. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/http_client.py +0 -0
  23. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/logging_config.py +0 -0
  24. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/models/__init__.py +0 -0
  25. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/models/base.py +0 -0
  26. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/models/comment.py +0 -0
  27. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/models/common.py +0 -0
  28. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/models/contractor.py +0 -0
  29. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/models/deal.py +0 -0
  30. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/models/department.py +0 -0
  31. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/models/employee.py +0 -0
  32. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/models/filter.py +0 -0
  33. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/models/group.py +0 -0
  34. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/models/milestone.py +0 -0
  35. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/models/participant.py +0 -0
  36. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/models/project.py +0 -0
  37. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/resources/__init__.py +0 -0
  38. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/resources/auth.py +0 -0
  39. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/resources/base.py +0 -0
  40. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/resources/contractors.py +0 -0
  41. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/resources/departments.py +0 -0
  42. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/resources/employees.py +0 -0
  43. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/resources/filters.py +0 -0
  44. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk/types.py +0 -0
  45. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk.egg-info/SOURCES.txt +0 -0
  46. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk.egg-info/dependency_links.txt +0 -0
  47. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk.egg-info/requires.txt +0 -0
  48. {megaplan_sdk-0.2.2 → megaplan_sdk-0.2.3}/src/megaplan_sdk.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: megaplan-sdk
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Professional Python SDK for Megaplan API v3
5
5
  Author-email: Maxim Borzov <max@borzov.com>
6
6
  License-Expression: MIT
@@ -73,6 +73,7 @@ Dynamic: license-file
73
73
  - [Глобальные дефолтные лимиты](#глобальные-дефолтные-лимиты)
74
74
  - [Автоматическая подгрузка связанных сущностей](#автоматическая-подгрузка-связанных-сущностей)
75
75
  - [Работа с фильтрами](#работа-с-фильтрами)
76
+ - [Работа с комментариями](#работа-с-комментариями)
76
77
  - [Настройка HTTP-клиента](#настройка-http-клиента)
77
78
  - [Работа через прокси](#работа-через-прокси)
78
79
  - [Ручное управление токенами](#ручное-управление-токенами)
@@ -429,7 +430,7 @@ print(details.deal.name) # для сделок
429
430
  # Связанные данные
430
431
  if details.comments:
431
432
  for comment in details.comments:
432
- print(comment.text)
433
+ print(comment.content)
433
434
 
434
435
  if details.history:
435
436
  print(f"Записей в истории: {len(details.history)}")
@@ -519,6 +520,37 @@ tasks = await client.tasks.list(filter=filter_obj)
519
520
  - `todos: list[BaseEntity]` - Подзадачи-чеклисты
520
521
  - `time_created: str` - Дата создания (API поле `timeCreated`)
521
522
  - `time_updated: str` - Дата обновления (API поле `timeUpdated`)
523
+ - `activity: str | None` - Дата последней активности (API поле `activity`)
524
+ - `last_comment_time_created: str | None` - Время последнего комментария (API поле `lastCommentTimeCreated`)
525
+ - `status_change_time: str | None` - Время смены статуса (API поле `statusChangeTime`)
526
+ - `actual_start: str | None` - Фактическое время начала (API поле `actualStart`)
527
+ - `last_view: str | None` - Время последнего просмотра (API поле `lastView`)
528
+
529
+ ### Фильтрация задач по временным полям
530
+
531
+ Временны́е поля (`activity`, `lastCommentTimeCreated` и др.) не возвращаются в `tasks.list()` по умолчанию.
532
+ Используйте константу `DEFAULT_TASK_LIST_FIELDS`, чтобы запросить их явно:
533
+
534
+ ```python
535
+ from megaplan_sdk import DEFAULT_TASK_LIST_FIELDS
536
+
537
+ # Запросить задачи с временными полями (activity, lastCommentTimeCreated и др.)
538
+ tasks = await client.tasks.list(
539
+ limit=50,
540
+ fields=list(DEFAULT_TASK_LIST_FIELDS),
541
+ )
542
+ for task in tasks:
543
+ print(f"{task.name}: активность {task.activity}")
544
+ ```
545
+
546
+ > **Примечание:** Сортировка по `"timeUpdated"` не поддерживается API — используйте `"activity"`:
547
+ >
548
+ > ```python
549
+ > tasks = await client.tasks.list(
550
+ > sort_by=[{"fieldName": "activity", "order": "desc"}],
551
+ > fields=list(DEFAULT_TASK_LIST_FIELDS),
552
+ > )
553
+ > ```
522
554
 
523
555
  ### Упрощенные методы создания
524
556
 
@@ -1502,6 +1534,56 @@ updated = await client.filters.update("task", filter_id=123, filter_config={...}
1502
1534
  export_data = await client.filters.export("task", filter_id=123)
1503
1535
  ```
1504
1536
 
1537
+ ### Работа с комментариями
1538
+
1539
+ Комментарии доступны для задач, проектов и сделок через `client.comments`.
1540
+
1541
+ #### Получение комментариев
1542
+
1543
+ ```python
1544
+ # Комментарии задачи (entity_type по умолчанию "task")
1545
+ comments = await client.comments.list(entity_id=42)
1546
+
1547
+ # Комментарии проекта
1548
+ comments = await client.comments.list(entity_id=5, entity_type="project")
1549
+
1550
+ # Комментарии сделки
1551
+ comments = await client.comments.list(entity_id=200, entity_type="deal")
1552
+
1553
+ # Автоматическая пагинация
1554
+ async for comment in client.comments.iterate(entity_id=42):
1555
+ print(comment.content)
1556
+ ```
1557
+
1558
+ #### Подгрузка авторов через expand
1559
+
1560
+ API Мегаплана не раскрывает поле `owner` (автор комментария) в списке комментариев.
1561
+ Используйте `expand=["owner"]`, чтобы SDK дозагрузил авторов отдельными запросами (с кэшированием):
1562
+
1563
+ ```python
1564
+ # Комментарии задачи с именами авторов
1565
+ comments = await client.comments.list(entity_id=42, expand=["owner"])
1566
+ for comment in comments:
1567
+ author_name = comment.owner.name if comment.owner else "неизвестен"
1568
+ print(f"{author_name}: {comment.content}")
1569
+ ```
1570
+
1571
+ #### Создание комментария
1572
+
1573
+ ```python
1574
+ # Комментарий к задаче
1575
+ comment = await client.comments.create(entity_id=42, comment_data={"text": "Текст комментария"})
1576
+
1577
+ # Комментарий к проекту
1578
+ comment = await client.comments.create(entity_id=5, comment_data={"text": "Текст"}, entity_type="project")
1579
+
1580
+ # Комментарий к сделке
1581
+ comment = await client.comments.create(entity_id=200, comment_data={"text": "Текст"}, entity_type="deal")
1582
+ ```
1583
+
1584
+ > **Ограничение:** Комментарии контрагентов не поддерживаются API (возвращает 500).
1585
+ > Используйте комментарии в связанных сделках или задачах.
1586
+
1505
1587
  ### Настройка HTTP-клиента
1506
1588
 
1507
1589
  ```python
@@ -41,6 +41,7 @@
41
41
  - [Глобальные дефолтные лимиты](#глобальные-дефолтные-лимиты)
42
42
  - [Автоматическая подгрузка связанных сущностей](#автоматическая-подгрузка-связанных-сущностей)
43
43
  - [Работа с фильтрами](#работа-с-фильтрами)
44
+ - [Работа с комментариями](#работа-с-комментариями)
44
45
  - [Настройка HTTP-клиента](#настройка-http-клиента)
45
46
  - [Работа через прокси](#работа-через-прокси)
46
47
  - [Ручное управление токенами](#ручное-управление-токенами)
@@ -397,7 +398,7 @@ print(details.deal.name) # для сделок
397
398
  # Связанные данные
398
399
  if details.comments:
399
400
  for comment in details.comments:
400
- print(comment.text)
401
+ print(comment.content)
401
402
 
402
403
  if details.history:
403
404
  print(f"Записей в истории: {len(details.history)}")
@@ -487,6 +488,37 @@ tasks = await client.tasks.list(filter=filter_obj)
487
488
  - `todos: list[BaseEntity]` - Подзадачи-чеклисты
488
489
  - `time_created: str` - Дата создания (API поле `timeCreated`)
489
490
  - `time_updated: str` - Дата обновления (API поле `timeUpdated`)
491
+ - `activity: str | None` - Дата последней активности (API поле `activity`)
492
+ - `last_comment_time_created: str | None` - Время последнего комментария (API поле `lastCommentTimeCreated`)
493
+ - `status_change_time: str | None` - Время смены статуса (API поле `statusChangeTime`)
494
+ - `actual_start: str | None` - Фактическое время начала (API поле `actualStart`)
495
+ - `last_view: str | None` - Время последнего просмотра (API поле `lastView`)
496
+
497
+ ### Фильтрация задач по временным полям
498
+
499
+ Временны́е поля (`activity`, `lastCommentTimeCreated` и др.) не возвращаются в `tasks.list()` по умолчанию.
500
+ Используйте константу `DEFAULT_TASK_LIST_FIELDS`, чтобы запросить их явно:
501
+
502
+ ```python
503
+ from megaplan_sdk import DEFAULT_TASK_LIST_FIELDS
504
+
505
+ # Запросить задачи с временными полями (activity, lastCommentTimeCreated и др.)
506
+ tasks = await client.tasks.list(
507
+ limit=50,
508
+ fields=list(DEFAULT_TASK_LIST_FIELDS),
509
+ )
510
+ for task in tasks:
511
+ print(f"{task.name}: активность {task.activity}")
512
+ ```
513
+
514
+ > **Примечание:** Сортировка по `"timeUpdated"` не поддерживается API — используйте `"activity"`:
515
+ >
516
+ > ```python
517
+ > tasks = await client.tasks.list(
518
+ > sort_by=[{"fieldName": "activity", "order": "desc"}],
519
+ > fields=list(DEFAULT_TASK_LIST_FIELDS),
520
+ > )
521
+ > ```
490
522
 
491
523
  ### Упрощенные методы создания
492
524
 
@@ -1470,6 +1502,56 @@ updated = await client.filters.update("task", filter_id=123, filter_config={...}
1470
1502
  export_data = await client.filters.export("task", filter_id=123)
1471
1503
  ```
1472
1504
 
1505
+ ### Работа с комментариями
1506
+
1507
+ Комментарии доступны для задач, проектов и сделок через `client.comments`.
1508
+
1509
+ #### Получение комментариев
1510
+
1511
+ ```python
1512
+ # Комментарии задачи (entity_type по умолчанию "task")
1513
+ comments = await client.comments.list(entity_id=42)
1514
+
1515
+ # Комментарии проекта
1516
+ comments = await client.comments.list(entity_id=5, entity_type="project")
1517
+
1518
+ # Комментарии сделки
1519
+ comments = await client.comments.list(entity_id=200, entity_type="deal")
1520
+
1521
+ # Автоматическая пагинация
1522
+ async for comment in client.comments.iterate(entity_id=42):
1523
+ print(comment.content)
1524
+ ```
1525
+
1526
+ #### Подгрузка авторов через expand
1527
+
1528
+ API Мегаплана не раскрывает поле `owner` (автор комментария) в списке комментариев.
1529
+ Используйте `expand=["owner"]`, чтобы SDK дозагрузил авторов отдельными запросами (с кэшированием):
1530
+
1531
+ ```python
1532
+ # Комментарии задачи с именами авторов
1533
+ comments = await client.comments.list(entity_id=42, expand=["owner"])
1534
+ for comment in comments:
1535
+ author_name = comment.owner.name if comment.owner else "неизвестен"
1536
+ print(f"{author_name}: {comment.content}")
1537
+ ```
1538
+
1539
+ #### Создание комментария
1540
+
1541
+ ```python
1542
+ # Комментарий к задаче
1543
+ comment = await client.comments.create(entity_id=42, comment_data={"text": "Текст комментария"})
1544
+
1545
+ # Комментарий к проекту
1546
+ comment = await client.comments.create(entity_id=5, comment_data={"text": "Текст"}, entity_type="project")
1547
+
1548
+ # Комментарий к сделке
1549
+ comment = await client.comments.create(entity_id=200, comment_data={"text": "Текст"}, entity_type="deal")
1550
+ ```
1551
+
1552
+ > **Ограничение:** Комментарии контрагентов не поддерживаются API (возвращает 500).
1553
+ > Используйте комментарии в связанных сделках или задачах.
1554
+
1473
1555
  ### Настройка HTTP-клиента
1474
1556
 
1475
1557
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "megaplan-sdk"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Professional Python SDK for Megaplan API v3"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,7 @@
1
1
  """Megaplan Python SDK - Professional SDK for Megaplan API v3."""
2
2
 
3
3
  from megaplan_sdk.client import MegaplanClient
4
+ from megaplan_sdk.constants import DEFAULT_TASK_LIST_FIELDS
4
5
  from megaplan_sdk.exceptions import (
5
6
  AuthenticationError,
6
7
  AuthorizationError,
@@ -101,6 +102,8 @@ __all__ = [
101
102
  "TaskFilterBuilder",
102
103
  "TradeFilterBuilder",
103
104
  "ProjectFilterBuilder",
105
+ # Constants
106
+ "DEFAULT_TASK_LIST_FIELDS",
104
107
  ]
105
108
 
106
- __version__ = "0.2.2"
109
+ __version__ = "0.2.3"
@@ -0,0 +1,42 @@
1
+ """Constants for Megaplan SDK."""
2
+
3
+
4
+ class ContentType:
5
+ """Content type constants for Megaplan API entities."""
6
+
7
+ TASK = "Task"
8
+ PROJECT = "Project"
9
+ DEAL = "Deal"
10
+ EMPLOYEE = "Employee"
11
+ CONTRACTOR = "Contractor"
12
+ CONTRACTOR_COMPANY = "ContractorCompany"
13
+ CONTRACTOR_HUMAN = "ContractorHuman"
14
+ CONTRACTOR_CATEGORY = "ContractorCategory"
15
+ DEPARTMENT = "Department"
16
+ COMMENT = "Comment"
17
+ GROUP = "Group"
18
+
19
+
20
+ # Task fields users commonly try to sort by that the Megaplan API rejects (422).
21
+ # Maps the unsupported field name to the supported replacement to suggest.
22
+ UNSUPPORTED_TASK_SORT_FIELDS: dict[str, str] = {
23
+ "timeUpdated": "activity",
24
+ "updatedAt": "activity",
25
+ }
26
+
27
+ # Recommended `fields` set for tasks.list() so that date fields are populated.
28
+ # Megaplan list endpoints omit these unless explicitly requested, which makes
29
+ # client-side time-window filtering silently return nothing (#8).
30
+ # Only fields confirmed to exist on Task are included (no "timeUpdated" — see #7).
31
+ DEFAULT_TASK_LIST_FIELDS: tuple[str, ...] = (
32
+ "name",
33
+ "status",
34
+ "timeCreated",
35
+ "activity",
36
+ "lastCommentTimeCreated",
37
+ "statusChangeTime",
38
+ "actualStart",
39
+ "owner",
40
+ "responsible",
41
+ "commentsCount",
42
+ )
@@ -44,6 +44,13 @@ class Task(TimestampMixin):
44
44
  tags: list[BaseEntity] | None = None
45
45
  attaches: list[BaseEntity] | None = None
46
46
  todos: list[BaseEntity] | None = None
47
+ activity: str | DateTime | None = None
48
+ last_comment_time_created: str | DateTime | None = Field(
49
+ alias="lastCommentTimeCreated", default=None
50
+ )
51
+ status_change_time: str | DateTime | None = Field(alias="statusChangeTime", default=None)
52
+ actual_start: str | DateTime | None = Field(alias="actualStart", default=None)
53
+ last_view: str | DateTime | None = Field(alias="lastView", default=None)
47
54
 
48
55
  model_config = ConfigDict(populate_by_name=True, extra="allow")
49
56
 
@@ -12,7 +12,12 @@ from megaplan_sdk.resources.base import BaseResource
12
12
  class CommentsResource(BaseResource):
13
13
  """Resource for working with comments."""
14
14
 
15
- async def create(self, entity_id: int, comment_data: dict[str, Any]) -> Comment:
15
+ async def create(
16
+ self,
17
+ entity_id: int,
18
+ comment_data: dict[str, Any],
19
+ entity_type: str = "task",
20
+ ) -> Comment:
16
21
  """Create a new comment for an entity.
17
22
 
18
23
  Args:
@@ -20,6 +25,9 @@ class CommentsResource(BaseResource):
20
25
  comment_data: Comment data dictionary.
21
26
  Required: text
22
27
  Optional: work (hours), attaches (file IDs)
28
+ entity_type: Entity type segment for the API path.
29
+ Allowed values: ``"task"`` | ``"project"`` | ``"deal"``.
30
+ Defaults to ``"task"``.
23
31
 
24
32
  Returns:
25
33
  Created comment.
@@ -30,14 +38,21 @@ class CommentsResource(BaseResource):
30
38
  ... entity_id=123,
31
39
  ... comment_data={"text": "Comment text", "work": 2.5}
32
40
  ... )
41
+ >>> # Create comment for project #55
42
+ >>> comment = await client.comments.create(
43
+ ... entity_id=55,
44
+ ... comment_data={"text": "Project note"},
45
+ ... entity_type="project",
46
+ ... )
33
47
  """
34
- path = self._build_path("api", "v3", "todo", str(entity_id), "comments")
48
+ path = self._build_path("api", "v3", entity_type, str(entity_id), "comments")
35
49
  response = await self._http.post(path, json_data=comment_data)
36
50
  return Comment(**response["data"])
37
51
 
38
52
  async def list(
39
53
  self,
40
54
  entity_id: int,
55
+ entity_type: str = "task",
41
56
  limit: int | None = None,
42
57
  page_after: dict[str, Any] | None = None,
43
58
  page_before: dict[str, Any] | None = None,
@@ -45,11 +60,15 @@ class CommentsResource(BaseResource):
45
60
  fields: Any | None = None,
46
61
  sort_by: list[dict[str, str]] | None = None,
47
62
  only_requested_fields: bool | None = None,
63
+ expand: list[str] | None = None,
48
64
  ) -> list[Comment]:
49
65
  """Get list of comments for an entity.
50
66
 
51
67
  Args:
52
68
  entity_id: Parent entity ID (task, project, deal, etc.).
69
+ entity_type: Entity type segment for the API path.
70
+ Allowed values: ``"task"`` | ``"project"`` | ``"deal"``.
71
+ Defaults to ``"task"``.
53
72
  limit: Number of items per page.
54
73
  page_after: Load page starting from this entity.
55
74
  page_before: Load page strictly before this entity.
@@ -57,6 +76,11 @@ class CommentsResource(BaseResource):
57
76
  fields: Additional fields to include.
58
77
  sort_by: Sort fields.
59
78
  only_requested_fields: Return only requested fields.
79
+ expand: Fields to expand. Supported values: ``"owner"``.
80
+ When ``"owner"`` is included, resolves Employee comment authors
81
+ to full Employee objects via batch parallel requests (cached).
82
+ The API never returns populated owners, so this is the only
83
+ resolution path.
60
84
 
61
85
  Returns:
62
86
  List of comments.
@@ -64,8 +88,13 @@ class CommentsResource(BaseResource):
64
88
  Examples:
65
89
  >>> # Get all comments for task #123
66
90
  >>> comments = await client.comments.list(entity_id=123)
91
+ >>> # Get all comments for project #55
92
+ >>> comments = await client.comments.list(entity_id=55, entity_type="project")
93
+ >>> # Resolve comment authors to full Employee objects
94
+ >>> comments = await client.comments.list(entity_id=123, expand=["owner"])
95
+ >>> print(comments[0].owner.name) # "Иван Петров"
67
96
  """
68
- path = self._build_path("api", "v3", "todo", str(entity_id), "comments")
97
+ path = self._build_path("api", "v3", entity_type, str(entity_id), "comments")
69
98
 
70
99
  # Use base method to build params (DRY)
71
100
  params = self._build_list_params(
@@ -78,7 +107,27 @@ class CommentsResource(BaseResource):
78
107
  only_requested_fields=only_requested_fields,
79
108
  )
80
109
 
81
- return await self._get_list(path, Comment, params)
110
+ comments = await self._get_list(path, Comment, params)
111
+
112
+ if not expand or "owner" not in expand or not comments:
113
+ return comments
114
+
115
+ from megaplan_sdk.models.employee import Employee
116
+
117
+ # Only Employee owners are resolvable via the employee endpoint.
118
+ employee_owners = [
119
+ c.owner for c in comments if c.owner is not None and c.owner.content_type == "Employee"
120
+ ]
121
+ owner_map = await self._load_related_entities(employee_owners, "employee", Employee)
122
+ for comment in comments:
123
+ if (
124
+ comment.owner is not None
125
+ and comment.owner.content_type == "Employee"
126
+ and comment.owner.id in owner_map
127
+ ):
128
+ comment.owner = owner_map[comment.owner.id]
129
+
130
+ return comments
82
131
 
83
132
  async def get(self, comment_id: int) -> Comment:
84
133
  """Get comment by ID.
@@ -119,6 +168,7 @@ class CommentsResource(BaseResource):
119
168
  async def iterate(
120
169
  self,
121
170
  entity_id: int,
171
+ entity_type: str = "task",
122
172
  limit: int = 100,
123
173
  **kwargs: Any,
124
174
  ) -> AsyncIterator[Comment]:
@@ -126,6 +176,9 @@ class CommentsResource(BaseResource):
126
176
 
127
177
  Args:
128
178
  entity_id: Parent entity ID (task, project, deal, etc.).
179
+ entity_type: Entity type segment for the API path.
180
+ Allowed values: ``"task"`` | ``"project"`` | ``"deal"``.
181
+ Defaults to ``"task"``.
129
182
  limit: Number of items per page.
130
183
  **kwargs: Additional parameters to pass to list().
131
184
 
@@ -136,6 +189,9 @@ class CommentsResource(BaseResource):
136
189
  >>> # Iterate over all comments for task #123
137
190
  >>> async for comment in client.comments.iterate(entity_id=123):
138
191
  ... print(comment.content)
192
+ >>> # Iterate over all comments for project #55
193
+ >>> async for comment in client.comments.iterate(entity_id=55, entity_type="project"):
194
+ ... print(comment.content)
139
195
  """
140
196
  comment: Comment
141
197
  async for comment in self._iterate_generic( # type: ignore[valid-type]
@@ -143,6 +199,7 @@ class CommentsResource(BaseResource):
143
199
  self.list,
144
200
  limit,
145
201
  entity_id=entity_id,
202
+ entity_type=entity_type,
146
203
  **kwargs,
147
204
  ):
148
205
  yield comment
@@ -710,10 +710,17 @@ class DealsResource(BaseResource, FullDetailsMixin):
710
710
  None = use global default (from MegaplanClient) or API default.
711
711
  Explicit value overrides global default.
712
712
  Example: comments_limit=50 returns max 50 comments.
713
+ Requires the matching include_* flag to be True; passing a
714
+ limit without it raises ValueError.
715
+ Note: the API caps a single comments page (~100); requesting
716
+ more returns at most one server page. Use client.comments.iterate
717
+ for full pagination.
713
718
  history_limit: Limit for history (if included).
714
719
  None = use global default (from MegaplanClient) or API default.
715
720
  Explicit value overrides global default.
716
721
  Example: history_limit=100 returns max 100 history entries.
722
+ Requires the matching include_* flag to be True; passing a
723
+ limit without it raises ValueError.
717
724
 
718
725
  Returns:
719
726
  DealFullDetails object with all requested data.
@@ -71,6 +71,21 @@ class FullDetailsMixin:
71
71
  getter = getattr(self, entity_getter)
72
72
  main_entity = await getter(entity_id)
73
73
 
74
+ # Guard: a *_limit without its include_* flag silently does nothing (#2).
75
+ # Reject it with a clear error instead of returning empty/stub data.
76
+ # This guard runs BEFORE the global-defaults block so that a client-level
77
+ # default (injected below) never triggers a false-positive error here.
78
+ for item_config in config:
79
+ limit_param = item_config.limit_param
80
+ if limit_param is None:
81
+ continue
82
+ if kwargs.get(limit_param) is not None and not kwargs.get(item_config.include_flag):
83
+ raise ValueError(
84
+ f"'{limit_param}' was provided but '{item_config.include_flag}' is False. "
85
+ f"Pass '{item_config.include_flag}=True' to load this data, "
86
+ f"or omit '{limit_param}'."
87
+ )
88
+
74
89
  # Apply global defaults for limit parameters if not explicitly provided
75
90
  # Note: parameters are always in kwargs (even if None), so check value instead of presence
76
91
  if hasattr(self, "_default_comments_limit") and kwargs.get("comments_limit") is None:
@@ -852,10 +852,17 @@ class ProjectsResource(BaseResource, FullDetailsMixin):
852
852
  None = use global default (from MegaplanClient) or API default.
853
853
  Explicit value overrides global default.
854
854
  Example: comments_limit=50 returns max 50 comments.
855
+ Requires the matching include_* flag to be True; passing a
856
+ limit without it raises ValueError.
857
+ Note: the API caps a single comments page (~100); requesting
858
+ more returns at most one server page. Use client.comments.iterate
859
+ for full pagination.
855
860
  history_limit: Limit for history (if included).
856
861
  None = use global default (from MegaplanClient) or API default.
857
862
  Explicit value overrides global default.
858
863
  Example: history_limit=100 returns max 100 history entries.
864
+ Requires the matching include_* flag to be True; passing a
865
+ limit without it raises ValueError.
859
866
 
860
867
  Returns:
861
868
  ProjectFullDetails object with all requested data.
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from collections.abc import AsyncIterator
6
6
  from typing import TYPE_CHECKING, Any, overload
7
7
 
8
- from megaplan_sdk.constants import ContentType
8
+ from megaplan_sdk.constants import UNSUPPORTED_TASK_SORT_FIELDS, ContentType
9
9
  from megaplan_sdk.models.comment import Comment
10
10
  from megaplan_sdk.models.task import Task, TaskFullDetails
11
11
  from megaplan_sdk.resources.base import BaseResource
@@ -219,6 +219,13 @@ class TasksResource(BaseResource, FullDetailsMixin):
219
219
  page_before: Load page strictly before this entity.
220
220
  page_with: Load page containing this entity.
221
221
  fields: Additional fields to include.
222
+ **Important:** list endpoints do NOT return date fields
223
+ (timeCreated, activity, lastCommentTimeCreated, ...) unless
224
+ requested here. To filter by a time window, pass:
225
+ from megaplan_sdk import DEFAULT_TASK_LIST_FIELDS
226
+ tasks = await client.tasks.list(fields=list(DEFAULT_TASK_LIST_FIELDS))
227
+ Without this, those fields are None and time-window filters
228
+ silently match nothing.
222
229
  sort_by: Sort fields.
223
230
  only_requested_fields: Return only requested fields.
224
231
  expand: List of fields to expand (e.g., ["responsible", "owner"]).
@@ -260,6 +267,18 @@ class TasksResource(BaseResource, FullDetailsMixin):
260
267
  f"Valid values: {sorted(VALID_TASK_STATUSES)}"
261
268
  )
262
269
 
270
+ # Validate sort_by against fields the API rejects with a raw 422 (#7).
271
+ if sort_by:
272
+ for rule in sort_by:
273
+ field_name = rule.get("fieldName")
274
+ if field_name in UNSUPPORTED_TASK_SORT_FIELDS:
275
+ suggestion = UNSUPPORTED_TASK_SORT_FIELDS[field_name]
276
+ raise ValueError(
277
+ f"Task cannot be sorted by '{field_name}' (API returns 422). "
278
+ f"Use '{suggestion}' instead — e.g. "
279
+ f'sort_by=[{{"fieldName": "{suggestion}", "desc": True}}].'
280
+ )
281
+
263
282
  # Convert filter ID to object format if needed
264
283
  processed_filter = filter
265
284
  if filter is not None and isinstance(filter, int | str) and not isinstance(filter, dict):
@@ -1064,10 +1083,17 @@ class TasksResource(BaseResource, FullDetailsMixin):
1064
1083
  None = use global default (from MegaplanClient) or API default.
1065
1084
  Explicit value overrides global default.
1066
1085
  Example: comments_limit=50 returns max 50 comments.
1086
+ Requires the matching include_* flag to be True; passing a
1087
+ limit without it raises ValueError.
1088
+ Note: the API caps a single comments page (~100); requesting
1089
+ more returns at most one server page. Use client.comments.iterate
1090
+ for full pagination.
1067
1091
  history_limit: Limit for history (if included).
1068
1092
  None = use global default (from MegaplanClient) or API default.
1069
1093
  Explicit value overrides global default.
1070
1094
  Example: history_limit=100 returns max 100 history entries.
1095
+ Requires the matching include_* flag to be True; passing a
1096
+ limit without it raises ValueError.
1071
1097
 
1072
1098
  Returns:
1073
1099
  TaskFullDetails object with all requested data.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: megaplan-sdk
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Professional Python SDK for Megaplan API v3
5
5
  Author-email: Maxim Borzov <max@borzov.com>
6
6
  License-Expression: MIT
@@ -73,6 +73,7 @@ Dynamic: license-file
73
73
  - [Глобальные дефолтные лимиты](#глобальные-дефолтные-лимиты)
74
74
  - [Автоматическая подгрузка связанных сущностей](#автоматическая-подгрузка-связанных-сущностей)
75
75
  - [Работа с фильтрами](#работа-с-фильтрами)
76
+ - [Работа с комментариями](#работа-с-комментариями)
76
77
  - [Настройка HTTP-клиента](#настройка-http-клиента)
77
78
  - [Работа через прокси](#работа-через-прокси)
78
79
  - [Ручное управление токенами](#ручное-управление-токенами)
@@ -429,7 +430,7 @@ print(details.deal.name) # для сделок
429
430
  # Связанные данные
430
431
  if details.comments:
431
432
  for comment in details.comments:
432
- print(comment.text)
433
+ print(comment.content)
433
434
 
434
435
  if details.history:
435
436
  print(f"Записей в истории: {len(details.history)}")
@@ -519,6 +520,37 @@ tasks = await client.tasks.list(filter=filter_obj)
519
520
  - `todos: list[BaseEntity]` - Подзадачи-чеклисты
520
521
  - `time_created: str` - Дата создания (API поле `timeCreated`)
521
522
  - `time_updated: str` - Дата обновления (API поле `timeUpdated`)
523
+ - `activity: str | None` - Дата последней активности (API поле `activity`)
524
+ - `last_comment_time_created: str | None` - Время последнего комментария (API поле `lastCommentTimeCreated`)
525
+ - `status_change_time: str | None` - Время смены статуса (API поле `statusChangeTime`)
526
+ - `actual_start: str | None` - Фактическое время начала (API поле `actualStart`)
527
+ - `last_view: str | None` - Время последнего просмотра (API поле `lastView`)
528
+
529
+ ### Фильтрация задач по временным полям
530
+
531
+ Временны́е поля (`activity`, `lastCommentTimeCreated` и др.) не возвращаются в `tasks.list()` по умолчанию.
532
+ Используйте константу `DEFAULT_TASK_LIST_FIELDS`, чтобы запросить их явно:
533
+
534
+ ```python
535
+ from megaplan_sdk import DEFAULT_TASK_LIST_FIELDS
536
+
537
+ # Запросить задачи с временными полями (activity, lastCommentTimeCreated и др.)
538
+ tasks = await client.tasks.list(
539
+ limit=50,
540
+ fields=list(DEFAULT_TASK_LIST_FIELDS),
541
+ )
542
+ for task in tasks:
543
+ print(f"{task.name}: активность {task.activity}")
544
+ ```
545
+
546
+ > **Примечание:** Сортировка по `"timeUpdated"` не поддерживается API — используйте `"activity"`:
547
+ >
548
+ > ```python
549
+ > tasks = await client.tasks.list(
550
+ > sort_by=[{"fieldName": "activity", "order": "desc"}],
551
+ > fields=list(DEFAULT_TASK_LIST_FIELDS),
552
+ > )
553
+ > ```
522
554
 
523
555
  ### Упрощенные методы создания
524
556
 
@@ -1502,6 +1534,56 @@ updated = await client.filters.update("task", filter_id=123, filter_config={...}
1502
1534
  export_data = await client.filters.export("task", filter_id=123)
1503
1535
  ```
1504
1536
 
1537
+ ### Работа с комментариями
1538
+
1539
+ Комментарии доступны для задач, проектов и сделок через `client.comments`.
1540
+
1541
+ #### Получение комментариев
1542
+
1543
+ ```python
1544
+ # Комментарии задачи (entity_type по умолчанию "task")
1545
+ comments = await client.comments.list(entity_id=42)
1546
+
1547
+ # Комментарии проекта
1548
+ comments = await client.comments.list(entity_id=5, entity_type="project")
1549
+
1550
+ # Комментарии сделки
1551
+ comments = await client.comments.list(entity_id=200, entity_type="deal")
1552
+
1553
+ # Автоматическая пагинация
1554
+ async for comment in client.comments.iterate(entity_id=42):
1555
+ print(comment.content)
1556
+ ```
1557
+
1558
+ #### Подгрузка авторов через expand
1559
+
1560
+ API Мегаплана не раскрывает поле `owner` (автор комментария) в списке комментариев.
1561
+ Используйте `expand=["owner"]`, чтобы SDK дозагрузил авторов отдельными запросами (с кэшированием):
1562
+
1563
+ ```python
1564
+ # Комментарии задачи с именами авторов
1565
+ comments = await client.comments.list(entity_id=42, expand=["owner"])
1566
+ for comment in comments:
1567
+ author_name = comment.owner.name if comment.owner else "неизвестен"
1568
+ print(f"{author_name}: {comment.content}")
1569
+ ```
1570
+
1571
+ #### Создание комментария
1572
+
1573
+ ```python
1574
+ # Комментарий к задаче
1575
+ comment = await client.comments.create(entity_id=42, comment_data={"text": "Текст комментария"})
1576
+
1577
+ # Комментарий к проекту
1578
+ comment = await client.comments.create(entity_id=5, comment_data={"text": "Текст"}, entity_type="project")
1579
+
1580
+ # Комментарий к сделке
1581
+ comment = await client.comments.create(entity_id=200, comment_data={"text": "Текст"}, entity_type="deal")
1582
+ ```
1583
+
1584
+ > **Ограничение:** Комментарии контрагентов не поддерживаются API (возвращает 500).
1585
+ > Используйте комментарии в связанных сделках или задачах.
1586
+
1505
1587
  ### Настройка HTTP-клиента
1506
1588
 
1507
1589
  ```python
@@ -1,17 +0,0 @@
1
- """Constants for Megaplan SDK."""
2
-
3
-
4
- class ContentType:
5
- """Content type constants for Megaplan API entities."""
6
-
7
- TASK = "Task"
8
- PROJECT = "Project"
9
- DEAL = "Deal"
10
- EMPLOYEE = "Employee"
11
- CONTRACTOR = "Contractor"
12
- CONTRACTOR_COMPANY = "ContractorCompany"
13
- CONTRACTOR_HUMAN = "ContractorHuman"
14
- CONTRACTOR_CATEGORY = "ContractorCategory"
15
- DEPARTMENT = "Department"
16
- COMMENT = "Comment"
17
- GROUP = "Group"
File without changes
File without changes