megaplan-sdk 0.2.3__tar.gz → 0.3.0__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 (51) hide show
  1. {megaplan_sdk-0.2.3/src/megaplan_sdk.egg-info → megaplan_sdk-0.3.0}/PKG-INFO +41 -1
  2. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/README.md +40 -0
  3. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/pyproject.toml +1 -1
  4. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/__init__.py +9 -1
  5. megaplan_sdk-0.3.0/src/megaplan_sdk/_knowledge_links.py +26 -0
  6. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/client.py +6 -0
  7. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/constants.py +2 -0
  8. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/__init__.py +8 -0
  9. megaplan_sdk-0.3.0/src/megaplan_sdk/models/knowledge.py +56 -0
  10. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/base.py +2 -0
  11. megaplan_sdk-0.3.0/src/megaplan_sdk/resources/knowledge_article.py +28 -0
  12. megaplan_sdk-0.3.0/src/megaplan_sdk/resources/knowledge_base.py +149 -0
  13. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0/src/megaplan_sdk.egg-info}/PKG-INFO +41 -1
  14. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk.egg-info/SOURCES.txt +4 -0
  15. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/LICENSE +0 -0
  16. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/setup.cfg +0 -0
  17. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/auth.py +0 -0
  18. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/cache.py +0 -0
  19. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/exceptions.py +0 -0
  20. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/filter_builder.py +0 -0
  21. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/helpers.py +0 -0
  22. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/http_client.py +0 -0
  23. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/logging_config.py +0 -0
  24. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/base.py +0 -0
  25. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/comment.py +0 -0
  26. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/common.py +0 -0
  27. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/contractor.py +0 -0
  28. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/deal.py +0 -0
  29. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/department.py +0 -0
  30. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/employee.py +0 -0
  31. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/filter.py +0 -0
  32. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/group.py +0 -0
  33. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/milestone.py +0 -0
  34. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/participant.py +0 -0
  35. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/project.py +0 -0
  36. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/models/task.py +0 -0
  37. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/__init__.py +0 -0
  38. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/auth.py +0 -0
  39. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/comments.py +0 -0
  40. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/contractors.py +0 -0
  41. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/deals.py +0 -0
  42. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/departments.py +0 -0
  43. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/employees.py +0 -0
  44. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/filters.py +0 -0
  45. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/full_details.py +0 -0
  46. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/projects.py +0 -0
  47. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/resources/tasks.py +0 -0
  48. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk/types.py +0 -0
  49. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk.egg-info/dependency_links.txt +0 -0
  50. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/src/megaplan_sdk.egg-info/requires.txt +0 -0
  51. {megaplan_sdk-0.2.3 → megaplan_sdk-0.3.0}/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.3
3
+ Version: 0.3.0
4
4
  Summary: Professional Python SDK for Megaplan API v3
5
5
  Author-email: Maxim Borzov <max@borzov.com>
6
6
  License-Expression: MIT
@@ -67,6 +67,7 @@ Dynamic: license-file
67
67
  - [Задачи](#работа-с-задачами)
68
68
  - [Проекты](#работа-с-проектами)
69
69
  - [Сделки](#работа-со-сделками)
70
+ - [База знаний](#работа-с-базой-знаний)
70
71
 
71
72
  ### Продвинутые возможности
72
73
  - [Кэширование сущностей](#кэширование-сущностей)
@@ -1123,6 +1124,45 @@ details = await client.deals.get_full_details(
1123
1124
 
1124
1125
  > **Примечание:** Общее описание метода `get_full_details()` и примеры использования см. в разделе [Общие паттерны работы с сущностями](#общие-паттерны-работы-с-сущностями).
1125
1126
 
1127
+ ## Работа с базой знаний
1128
+
1129
+ SDK предоставляет доступ к разделам и статьям Базы знаний Мегаплана через два ресурса: `client.knowledge_base` (разделы) и `client.knowledge_article` (статьи).
1130
+
1131
+ ```python
1132
+ # Список разделов Базы знаний (плоский)
1133
+ sections = await client.knowledge_base.list()
1134
+ for s in sections:
1135
+ print(s.id, s.name)
1136
+
1137
+ # Один раздел с HTML-содержимым
1138
+ section = await client.knowledge_base.get(11)
1139
+ print(section.content)
1140
+
1141
+ # Итерация по всем разделам с автопагинацией
1142
+ async for section in client.knowledge_base.iterate():
1143
+ print(section.id, section.name)
1144
+
1145
+ # Статья по ID (parent всегда None — используйте base)
1146
+ article = await client.knowledge_article.get(33)
1147
+ print(article.name, "→ раздел:", article.base.name if article.base else None)
1148
+
1149
+ # Экспериментально: раздел вместе со статьями (через парсинг HTML-ссылок)
1150
+ bundle = await client.knowledge_base.get_with_articles(2)
1151
+ for a in bundle.articles:
1152
+ print(a.id, a.name)
1153
+ ```
1154
+
1155
+ > **Известные ограничения API (серверная сторона):**
1156
+ >
1157
+ > - **Нет листинга статей:** эндпоинт `GET /api/v3/knowledgeArticle` отсутствует (возвращает 404).
1158
+ > Единственный способ обнаружения статей в разделе — метод `get_with_articles()`, который
1159
+ > парсит HTML-ссылки из поля `content` раздела. Это **экспериментальный** и хрупкий метод —
1160
+ > формат ссылок может измениться на стороне сервера.
1161
+ > - **Фильтр `parent` не работает:** `knowledge_base.list()` всегда возвращает плоский список
1162
+ > всех разделов; передача `parent` в фильтре игнорируется сервером. Иерархии разделов нет.
1163
+ > - **Поле `parent` у статьи всегда `null`:** для определения принадлежности статьи к разделу
1164
+ > используйте `article.base` (не `article.parent`).
1165
+
1126
1166
  ## Продвинутые возможности
1127
1167
 
1128
1168
  ### Кэширование сущностей
@@ -35,6 +35,7 @@
35
35
  - [Задачи](#работа-с-задачами)
36
36
  - [Проекты](#работа-с-проектами)
37
37
  - [Сделки](#работа-со-сделками)
38
+ - [База знаний](#работа-с-базой-знаний)
38
39
 
39
40
  ### Продвинутые возможности
40
41
  - [Кэширование сущностей](#кэширование-сущностей)
@@ -1091,6 +1092,45 @@ details = await client.deals.get_full_details(
1091
1092
 
1092
1093
  > **Примечание:** Общее описание метода `get_full_details()` и примеры использования см. в разделе [Общие паттерны работы с сущностями](#общие-паттерны-работы-с-сущностями).
1093
1094
 
1095
+ ## Работа с базой знаний
1096
+
1097
+ SDK предоставляет доступ к разделам и статьям Базы знаний Мегаплана через два ресурса: `client.knowledge_base` (разделы) и `client.knowledge_article` (статьи).
1098
+
1099
+ ```python
1100
+ # Список разделов Базы знаний (плоский)
1101
+ sections = await client.knowledge_base.list()
1102
+ for s in sections:
1103
+ print(s.id, s.name)
1104
+
1105
+ # Один раздел с HTML-содержимым
1106
+ section = await client.knowledge_base.get(11)
1107
+ print(section.content)
1108
+
1109
+ # Итерация по всем разделам с автопагинацией
1110
+ async for section in client.knowledge_base.iterate():
1111
+ print(section.id, section.name)
1112
+
1113
+ # Статья по ID (parent всегда None — используйте base)
1114
+ article = await client.knowledge_article.get(33)
1115
+ print(article.name, "→ раздел:", article.base.name if article.base else None)
1116
+
1117
+ # Экспериментально: раздел вместе со статьями (через парсинг HTML-ссылок)
1118
+ bundle = await client.knowledge_base.get_with_articles(2)
1119
+ for a in bundle.articles:
1120
+ print(a.id, a.name)
1121
+ ```
1122
+
1123
+ > **Известные ограничения API (серверная сторона):**
1124
+ >
1125
+ > - **Нет листинга статей:** эндпоинт `GET /api/v3/knowledgeArticle` отсутствует (возвращает 404).
1126
+ > Единственный способ обнаружения статей в разделе — метод `get_with_articles()`, который
1127
+ > парсит HTML-ссылки из поля `content` раздела. Это **экспериментальный** и хрупкий метод —
1128
+ > формат ссылок может измениться на стороне сервера.
1129
+ > - **Фильтр `parent` не работает:** `knowledge_base.list()` всегда возвращает плоский список
1130
+ > всех разделов; передача `parent` в фильтре игнорируется сервером. Иерархии разделов нет.
1131
+ > - **Поле `parent` у статьи всегда `null`:** для определения принадлежности статьи к разделу
1132
+ > используйте `article.base` (не `article.parent`).
1133
+
1094
1134
  ## Продвинутые возможности
1095
1135
 
1096
1136
  ### Кэширование сущностей
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "megaplan-sdk"
7
- version = "0.2.3"
7
+ version = "0.3.0"
8
8
  description = "Professional Python SDK for Megaplan API v3"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -42,6 +42,11 @@ from megaplan_sdk.models.filter import (
42
42
  UserSetting,
43
43
  )
44
44
  from megaplan_sdk.models.group import Group
45
+ from megaplan_sdk.models.knowledge import (
46
+ KnowledgeArticle,
47
+ KnowledgeBase,
48
+ KnowledgeSectionWithArticles,
49
+ )
45
50
  from megaplan_sdk.models.milestone import Milestone
46
51
  from megaplan_sdk.models.participant import Participant, parse_participant, parse_participants
47
52
  from megaplan_sdk.models.project import Project, ProjectFullDetails
@@ -75,6 +80,9 @@ __all__ = [
75
80
  "Department",
76
81
  "Group",
77
82
  "Milestone",
83
+ "KnowledgeBase",
84
+ "KnowledgeArticle",
85
+ "KnowledgeSectionWithArticles",
78
86
  "Participant",
79
87
  "parse_participant",
80
88
  "parse_participants",
@@ -106,4 +114,4 @@ __all__ = [
106
114
  "DEFAULT_TASK_LIST_FIELDS",
107
115
  ]
108
116
 
109
- __version__ = "0.2.3"
117
+ __version__ = "0.3.0"
@@ -0,0 +1,26 @@
1
+ """HTML link parsing for Knowledge Base article discovery.
2
+
3
+ EXPERIMENTAL / FRAGILE: this module is the single place coupled to Megaplan's
4
+ HTML format. The Megaplan API has no endpoint to list articles in a section,
5
+ so article IDs are discovered by parsing ``/knowledge/<section>/<article>``
6
+ links inside a section's HTML ``content``.
7
+ """
8
+
9
+ import re
10
+
11
+ # Matches /knowledge/<section_id> and /knowledge/<section_id>/<article_id>
12
+ # inside href values; scheme/host-agnostic (absolute or relative URLs).
13
+ _KNOWLEDGE_LINK_RE = re.compile(r"/knowledge/(\d+)(?:/(\d+))?")
14
+
15
+
16
+ def extract_article_ids(html: str | None) -> list[int]:
17
+ """Extract article IDs from two-segment ``/knowledge/<section>/<article>`` links.
18
+
19
+ Single-segment ``/knowledge/<section>`` links (section references, e.g. a
20
+ table of contents) are ignored. Returns unique IDs in first-seen order.
21
+ """
22
+ seen: dict[int, None] = {}
23
+ for _section, article in _KNOWLEDGE_LINK_RE.findall(html or ""):
24
+ if article:
25
+ seen.setdefault(int(article), None)
26
+ return list(seen)
@@ -13,6 +13,8 @@ from megaplan_sdk.resources.deals import DealsResource
13
13
  from megaplan_sdk.resources.departments import DepartmentsResource
14
14
  from megaplan_sdk.resources.employees import EmployeesResource
15
15
  from megaplan_sdk.resources.filters import FiltersResource
16
+ from megaplan_sdk.resources.knowledge_article import KnowledgeArticleResource
17
+ from megaplan_sdk.resources.knowledge_base import KnowledgeBaseResource
16
18
  from megaplan_sdk.resources.projects import ProjectsResource
17
19
  from megaplan_sdk.resources.tasks import TasksResource
18
20
 
@@ -119,6 +121,10 @@ class MegaplanClient:
119
121
  self.employees = EmployeesResource(self._http, cache=self._cache)
120
122
  self.departments = DepartmentsResource(self._http, cache=self._cache)
121
123
  self.filters = FiltersResource(self._http, cache=self._cache)
124
+ self.knowledge_article = KnowledgeArticleResource(self._http, cache=self._cache)
125
+ self.knowledge_base = KnowledgeBaseResource(
126
+ self._http, cache=self._cache, article_resource=self.knowledge_article
127
+ )
122
128
 
123
129
  # Security: Store password only for initial authentication if provided
124
130
  self._initial_password = password if (username and password) else None
@@ -15,6 +15,8 @@ class ContentType:
15
15
  DEPARTMENT = "Department"
16
16
  COMMENT = "Comment"
17
17
  GROUP = "Group"
18
+ KNOWLEDGE_BASE = "KnowledgeBase"
19
+ KNOWLEDGE_ARTICLE = "KnowledgeArticle"
18
20
 
19
21
 
20
22
  # Task fields users commonly try to sort by that the Megaplan API rejects (422).
@@ -4,6 +4,11 @@ from megaplan_sdk.models.base import BaseEntity
4
4
  from megaplan_sdk.models.common import File, Meta, Pagination, SortField
5
5
  from megaplan_sdk.models.deal import Deal, ProgramState, TradeFilter
6
6
  from megaplan_sdk.models.group import Group
7
+ from megaplan_sdk.models.knowledge import (
8
+ KnowledgeArticle,
9
+ KnowledgeBase,
10
+ KnowledgeSectionWithArticles,
11
+ )
7
12
  from megaplan_sdk.models.participant import Participant, parse_participant, parse_participants
8
13
  from megaplan_sdk.models.project import Project, ProjectFilter
9
14
  from megaplan_sdk.models.task import Task, TaskFilter
@@ -25,4 +30,7 @@ __all__ = [
25
30
  "Participant",
26
31
  "parse_participant",
27
32
  "parse_participants",
33
+ "KnowledgeBase",
34
+ "KnowledgeArticle",
35
+ "KnowledgeSectionWithArticles",
28
36
  ]
@@ -0,0 +1,56 @@
1
+ """Knowledge Base models for Megaplan SDK."""
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+ from megaplan_sdk.models.base import BaseEntity
6
+ from megaplan_sdk.models.common import DateTime
7
+
8
+
9
+ class KnowledgeBase(BaseEntity):
10
+ """Knowledge Base section.
11
+
12
+ The list endpoint returns a trimmed object (no ``content`` /
13
+ ``last_updated`` / ``is_dropped``); ``knowledge_base.get(id)`` returns
14
+ the full object including the HTML ``content``.
15
+ """
16
+
17
+ content_type: str = Field(alias="contentType", default="KnowledgeBase")
18
+ content: str | None = None # HTML, present only in the full get() response
19
+ access_role: str | None = Field(alias="accessRole", default=None)
20
+ last_updated: DateTime | None = Field(alias="lastUpdated", default=None)
21
+ last_update_by: BaseEntity | None = Field(alias="lastUpdateBy", default=None)
22
+ order_pos: int | None = Field(alias="orderPos", default=None)
23
+ expanded: bool | None = None
24
+ is_dropped: bool | None = Field(alias="isDropped", default=None)
25
+
26
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
27
+
28
+
29
+ class KnowledgeArticle(BaseEntity):
30
+ """Knowledge Base article.
31
+
32
+ NOTE: ``parent`` is ALWAYS null in the Megaplan API — use ``base`` (the
33
+ parent section reference) to determine which section the article belongs to.
34
+ """
35
+
36
+ content_type: str = Field(alias="contentType", default="KnowledgeArticle")
37
+ content: str | None = None # HTML body
38
+ parent: BaseEntity | None = None # always null in API; use `base`
39
+ base: KnowledgeBase | None = None # the real parent-section link
40
+ access_role: str | None = Field(alias="accessRole", default=None)
41
+ last_updated: DateTime | None = Field(alias="lastUpdated", default=None)
42
+ last_update_by: BaseEntity | None = Field(alias="lastUpdateBy", default=None)
43
+ order_pos: int | None = Field(alias="orderPos", default=None)
44
+ expanded: bool | None = None
45
+ is_dropped: bool | None = Field(alias="isDropped", default=None)
46
+
47
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
48
+
49
+
50
+ class KnowledgeSectionWithArticles(BaseModel):
51
+ """Composite returned by the experimental ``get_with_articles`` helper."""
52
+
53
+ section: KnowledgeBase
54
+ articles: list[KnowledgeArticle]
55
+
56
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
@@ -521,6 +521,8 @@ class BaseResource:
521
521
  "contractorCompany": ContentType.CONTRACTOR_COMPANY,
522
522
  "contractorHuman": ContentType.CONTRACTOR_HUMAN,
523
523
  "comment": ContentType.COMMENT,
524
+ "knowledgeBase": ContentType.KNOWLEDGE_BASE,
525
+ "knowledgeArticle": ContentType.KNOWLEDGE_ARTICLE,
524
526
  }
525
527
 
526
528
  result = mapping.get(entity_type)
@@ -0,0 +1,28 @@
1
+ """Knowledge Base article resource for Megaplan API."""
2
+
3
+ from megaplan_sdk.models.knowledge import KnowledgeArticle
4
+ from megaplan_sdk.resources.base import BaseResource
5
+
6
+
7
+ class KnowledgeArticleResource(BaseResource):
8
+ """Resource for Knowledge Base articles.
9
+
10
+ Only ``get(id)`` is supported: the Megaplan API has NO endpoint to list
11
+ articles (``GET /api/v3/knowledgeArticle`` returns 404). To discover the
12
+ articles of a section, use ``client.knowledge_base.get_with_articles()``.
13
+ """
14
+
15
+ async def get(self, article_id: int, use_cache: bool = True) -> KnowledgeArticle:
16
+ """Get a Knowledge Base article by ID.
17
+
18
+ Args:
19
+ article_id: Article identifier.
20
+ use_cache: Whether to use the entity cache (default: True).
21
+
22
+ Returns:
23
+ The article, including its ``base`` (parent section) reference.
24
+ Note: ``article.parent`` is always None in the API — use ``base``.
25
+ """
26
+ return await self._get_entity_cached(
27
+ "knowledgeArticle", article_id, KnowledgeArticle, use_cache=use_cache
28
+ )
@@ -0,0 +1,149 @@
1
+ """Knowledge Base section resource for Megaplan API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import AsyncIterator
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from megaplan_sdk._knowledge_links import extract_article_ids
10
+ from megaplan_sdk.http_client import HTTPClient
11
+ from megaplan_sdk.logging_config import logger
12
+ from megaplan_sdk.models.knowledge import (
13
+ KnowledgeArticle,
14
+ KnowledgeBase,
15
+ KnowledgeSectionWithArticles,
16
+ )
17
+ from megaplan_sdk.resources.base import BaseResource
18
+ from megaplan_sdk.resources.knowledge_article import KnowledgeArticleResource
19
+
20
+ if TYPE_CHECKING:
21
+ from megaplan_sdk.cache import EntityCache
22
+
23
+
24
+ class KnowledgeBaseResource(BaseResource):
25
+ """Resource for Knowledge Base sections.
26
+
27
+ Provides ``get`` / ``list`` / ``iterate`` over the working endpoints, plus
28
+ the experimental ``get_with_articles`` helper.
29
+
30
+ NOTE: the server-side ``parent`` filter on ``GET /api/v3/knowledgeBase`` is
31
+ ignored and sections are flat (no hierarchy), so ``list`` exposes no
32
+ ``parent`` parameter. The real hierarchy is section -> articles via
33
+ ``KnowledgeArticle.base``.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ http_client: HTTPClient,
39
+ cache: EntityCache | None = None,
40
+ article_resource: KnowledgeArticleResource | None = None,
41
+ ) -> None:
42
+ super().__init__(http_client, cache=cache)
43
+ # Reused for get_with_articles; falls back to a self-built one (shared cache).
44
+ self._article_resource = article_resource or KnowledgeArticleResource(
45
+ http_client, cache=cache
46
+ )
47
+
48
+ async def get(self, section_id: int, use_cache: bool = True) -> KnowledgeBase:
49
+ """Get a Knowledge Base section by ID (full object, including HTML content).
50
+
51
+ Args:
52
+ section_id: Section identifier.
53
+ use_cache: Whether to use the entity cache (default: True).
54
+
55
+ Returns:
56
+ Full KnowledgeBase section including HTML content.
57
+ """
58
+ return await self._get_entity_cached(
59
+ "knowledgeBase", section_id, KnowledgeBase, use_cache=use_cache
60
+ )
61
+
62
+ async def list(
63
+ self,
64
+ limit: int | None = None,
65
+ page_after: dict[str, Any] | None = None,
66
+ page_before: dict[str, Any] | None = None,
67
+ page_with: dict[str, Any] | None = None,
68
+ ) -> list[KnowledgeBase]:
69
+ """Get the flat list of Knowledge Base sections.
70
+
71
+ The API has no working ``parent`` filter and sections have no
72
+ hierarchy, so all sections are returned as a flat list.
73
+
74
+ Args:
75
+ limit: Number of items per page.
76
+ page_after: Load page starting from this entity.
77
+ page_before: Load page strictly before this entity.
78
+ page_with: Load page containing this entity.
79
+
80
+ Returns:
81
+ List of KnowledgeBase sections.
82
+ """
83
+ path = self._build_path("api", "v3", "knowledgeBase")
84
+ params = self._build_list_params(
85
+ limit=limit,
86
+ page_after=page_after,
87
+ page_before=page_before,
88
+ page_with=page_with,
89
+ )
90
+ return await self._get_list(path, KnowledgeBase, params)
91
+
92
+ async def iterate(self, limit: int = 100, **kwargs: Any) -> AsyncIterator[KnowledgeBase]:
93
+ """Iterate over all Knowledge Base sections with automatic pagination.
94
+
95
+ Args:
96
+ limit: Number of items per page.
97
+ **kwargs: Additional parameters to pass to list().
98
+
99
+ Yields:
100
+ KnowledgeBase section objects.
101
+ """
102
+ section: KnowledgeBase
103
+ async for section in self._iterate_generic( # type: ignore[valid-type]
104
+ "KnowledgeBase",
105
+ self.list,
106
+ limit,
107
+ **kwargs,
108
+ ):
109
+ yield section
110
+
111
+ async def get_with_articles(
112
+ self, section_id: int, use_cache: bool = True
113
+ ) -> KnowledgeSectionWithArticles:
114
+ """Get a section together with its articles (EXPERIMENTAL).
115
+
116
+ The Megaplan API has no endpoint to list a section's articles, so this
117
+ discovers them by parsing ``/knowledge/<section>/<article>`` links in
118
+ the section's HTML ``content`` and fetching each article. Only articles
119
+ whose ``base.id`` equals ``section_id`` are kept (so a table-of-contents
120
+ section cannot pull in unrelated articles). Individual article failures
121
+ are skipped with a warning.
122
+
123
+ FRAGILE: depends on Megaplan's HTML link format. If the format changes,
124
+ ``articles`` may come back empty even when the section has articles.
125
+
126
+ Args:
127
+ section_id: Section identifier.
128
+ use_cache: Whether to use the entity cache (default: True).
129
+
130
+ Returns:
131
+ KnowledgeSectionWithArticles with section and its filtered articles.
132
+ """
133
+ section = await self.get(section_id, use_cache=use_cache)
134
+ article_ids = extract_article_ids(section.content)
135
+
136
+ fetched = await asyncio.gather(
137
+ *(self._article_resource.get(aid, use_cache=use_cache) for aid in article_ids),
138
+ return_exceptions=True,
139
+ )
140
+
141
+ articles: list[KnowledgeArticle] = []
142
+ for aid, result in zip(article_ids, fetched, strict=True):
143
+ if isinstance(result, BaseException):
144
+ logger.warning("Failed to load knowledge article %s: %s", aid, result)
145
+ continue
146
+ if result.base is not None and result.base.id == section_id:
147
+ articles.append(result)
148
+
149
+ return KnowledgeSectionWithArticles(section=section, articles=articles)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: megaplan-sdk
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: Professional Python SDK for Megaplan API v3
5
5
  Author-email: Maxim Borzov <max@borzov.com>
6
6
  License-Expression: MIT
@@ -67,6 +67,7 @@ Dynamic: license-file
67
67
  - [Задачи](#работа-с-задачами)
68
68
  - [Проекты](#работа-с-проектами)
69
69
  - [Сделки](#работа-со-сделками)
70
+ - [База знаний](#работа-с-базой-знаний)
70
71
 
71
72
  ### Продвинутые возможности
72
73
  - [Кэширование сущностей](#кэширование-сущностей)
@@ -1123,6 +1124,45 @@ details = await client.deals.get_full_details(
1123
1124
 
1124
1125
  > **Примечание:** Общее описание метода `get_full_details()` и примеры использования см. в разделе [Общие паттерны работы с сущностями](#общие-паттерны-работы-с-сущностями).
1125
1126
 
1127
+ ## Работа с базой знаний
1128
+
1129
+ SDK предоставляет доступ к разделам и статьям Базы знаний Мегаплана через два ресурса: `client.knowledge_base` (разделы) и `client.knowledge_article` (статьи).
1130
+
1131
+ ```python
1132
+ # Список разделов Базы знаний (плоский)
1133
+ sections = await client.knowledge_base.list()
1134
+ for s in sections:
1135
+ print(s.id, s.name)
1136
+
1137
+ # Один раздел с HTML-содержимым
1138
+ section = await client.knowledge_base.get(11)
1139
+ print(section.content)
1140
+
1141
+ # Итерация по всем разделам с автопагинацией
1142
+ async for section in client.knowledge_base.iterate():
1143
+ print(section.id, section.name)
1144
+
1145
+ # Статья по ID (parent всегда None — используйте base)
1146
+ article = await client.knowledge_article.get(33)
1147
+ print(article.name, "→ раздел:", article.base.name if article.base else None)
1148
+
1149
+ # Экспериментально: раздел вместе со статьями (через парсинг HTML-ссылок)
1150
+ bundle = await client.knowledge_base.get_with_articles(2)
1151
+ for a in bundle.articles:
1152
+ print(a.id, a.name)
1153
+ ```
1154
+
1155
+ > **Известные ограничения API (серверная сторона):**
1156
+ >
1157
+ > - **Нет листинга статей:** эндпоинт `GET /api/v3/knowledgeArticle` отсутствует (возвращает 404).
1158
+ > Единственный способ обнаружения статей в разделе — метод `get_with_articles()`, который
1159
+ > парсит HTML-ссылки из поля `content` раздела. Это **экспериментальный** и хрупкий метод —
1160
+ > формат ссылок может измениться на стороне сервера.
1161
+ > - **Фильтр `parent` не работает:** `knowledge_base.list()` всегда возвращает плоский список
1162
+ > всех разделов; передача `parent` в фильтре игнорируется сервером. Иерархии разделов нет.
1163
+ > - **Поле `parent` у статьи всегда `null`:** для определения принадлежности статьи к разделу
1164
+ > используйте `article.base` (не `article.parent`).
1165
+
1126
1166
  ## Продвинутые возможности
1127
1167
 
1128
1168
  ### Кэширование сущностей
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  src/megaplan_sdk/__init__.py
5
+ src/megaplan_sdk/_knowledge_links.py
5
6
  src/megaplan_sdk/auth.py
6
7
  src/megaplan_sdk/cache.py
7
8
  src/megaplan_sdk/client.py
@@ -27,6 +28,7 @@ src/megaplan_sdk/models/department.py
27
28
  src/megaplan_sdk/models/employee.py
28
29
  src/megaplan_sdk/models/filter.py
29
30
  src/megaplan_sdk/models/group.py
31
+ src/megaplan_sdk/models/knowledge.py
30
32
  src/megaplan_sdk/models/milestone.py
31
33
  src/megaplan_sdk/models/participant.py
32
34
  src/megaplan_sdk/models/project.py
@@ -41,5 +43,7 @@ src/megaplan_sdk/resources/departments.py
41
43
  src/megaplan_sdk/resources/employees.py
42
44
  src/megaplan_sdk/resources/filters.py
43
45
  src/megaplan_sdk/resources/full_details.py
46
+ src/megaplan_sdk/resources/knowledge_article.py
47
+ src/megaplan_sdk/resources/knowledge_base.py
44
48
  src/megaplan_sdk/resources/projects.py
45
49
  src/megaplan_sdk/resources/tasks.py
File without changes
File without changes